diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 1654b9179b..4b0f9f762c 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -1,7 +1,12 @@ import operator import ifcopenshell.util.element +import re +from xmlschema import XMLSchema +from xmlschema import etree_tostring +from xmlschema.validators import facets +from xmlschema.validators import identities -from xml.dom.minidom import parse +ids_schema = XMLSchema("http://standards.buildingsmart.org/IDS/ids.xsd") class exception(Exception): @@ -46,22 +51,27 @@ 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 __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): - 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]) + if k in self.node: + v = self.node[k] + if isinstance(v, dict): #is restriction? + return restriction(v['xs:restriction'][0]) else: - return v.firstChild.nodeValue.strip() + return v else: return None @@ -84,94 +94,248 @@ class entity(facet): parameters = ["name", "predefinedtype"] + def create(name=None, predefinedtype=None): + inst = entity() + inst.name = name + inst.predefinedtype = predefinedtype + return inst + + 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"): - # 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): """ 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 create(location='any', value=None, system=None): + inst = classification() + inst.location = location + inst.value = value + inst.system = system + return inst + + def asdict(self): + fac_dict = { + '@location': self.location, + 'value': self.value, + 'system': self.system + } + return fac_dict def __call__(self, inst, logger): + + 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 - refs.append((cref.ReferencedSource.Name, cref.ItemReference)) + if hasattr(cref, 'ItemReference'): #IFC2x3 + refs.append((cref.ReferencedSource.Name, cref.ItemReference)) + elif hasattr(cref, 'Identification'): # IFC4 + refs.append((cref.ReferencedSource.Name, cref.Identification)) - return facet_evaluation( - (self.system, self.value) in refs, - # @todo - "[classification_eval_todo]", - ) + 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_msg} # what if not first item of refs? + ) + else: + return facet_evaluation( + False, + "does not have %sclassification reference" % self.location_msg + ) 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"] + parameters = ["name", "propertyset", "value", "location"] + message = "%(location)sproperty '%(name)s' in '%(propertyset)s' with a value %(value)s" - # import pdb;pdb.set_trace() - message = "a property '%(name)s' in '%(propertyset)s' with 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, + '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): - 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 - logger.debug("Testing if property %s == %s", val, self.value) - + + self.location_msg = location[self.location] di = { "name": self.name, "propertyset": self.propertyset, - "value": val, + "value": "'%s'" % val, + "location": self.location_msg } if val is not None: msg = self.message % di else: if pset: - msg = "a set '%(propertyset)s', but no property '%(name)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 - return facet_evaluation(val == self.value, msg) + #TODO implement data type comparison + return facet_evaluation( + val == 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", "location"] + 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' + # # BUG '@use': 'optional' + # # BUG '@href': 'https://identifier.buildingsmart.org/uri/something', + # # BUG 'instructions': 'Please add the desired...', + return inst + + 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): - material_relations = [rel for rel in inst.HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] - names = [] + + 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 not all subtypes of IfcMaterial handled - if rel.RelatingMaterial.is_a() == "IfcMaterialLayerSetUsage": + 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'}) + + if not materials: + materials.append('UNDEFINED') + + self.location_msg = location[self.location] + return facet_evaluation( - 0, - # @todo - "[material_eval_todo]", + self.value in materials, + self.message % {"value": "'/'".join(materials), "location": self.location_msg}, ) @@ -186,7 +350,10 @@ class boolean_logic: def __call__(self, *args): 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)) @@ -207,37 +374,84 @@ class restriction: def __init__(self, node): - self.restriction_on = node.getAttribute("base") + self.restriction_on = node['@base'][3:] + self.type = "" 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" + + for n in node: + if n[0:3] == "xs:": + if n[3:] == "enumeration": + self.type = "enumeration" + 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'] + 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'}) def __eq__(self, other): - return other in self.options + 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": + 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 + return result 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, self.options[0], 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, 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]) + return "of type '%s' respecting pattern '%s'" % (self.restriction_on, ' and '.join(self.options)) + #TODO add fractionDigits + #TODO add totalDigits + #TODO add whiteSpace class specification: @@ -245,29 +459,81 @@ 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, + '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 + + @staticmethod + def parse(node): def parse_rules(node): - children = [n for n in node.childNodes if n.nodeType == n.ELEMENT_NODE] - names = map(operator.attrgetter("tagName"), children) + 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 - 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) + # 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): + 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"}) + return True, True 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"}) + # 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 def __str__(self): return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ @@ -278,34 +544,95 @@ class ids: Represents the XML root node and its childNodes. """ - def __init__(self, fn): - dom = parse(fn) - ids = dom.childNodes[0] - ids.tagName == "ids" or error("expected ") + 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', + '@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 + + 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) + f.close() + + ids_schema.validate(fn) + return ids_schema.is_valid(fn) + + @staticmethod + def parse(fn, ids_schema=ids_schema): + ids_schema.validate(fn) + ids_content = ids_schema.decode(fn) + new_ids = ids() + new_ids.specifications = [specification.parse(s) for s in ids_content['specification']] + return new_ids - self.specifications = [ - specification(n) for n in ids.childNodes if n.nodeType == n.ELEMENT_NODE and n.tagName == "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 + + + +location = { + 'instance': 'an instance ', + 'type': 'a type ', + 'any': 'a ' +} + 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") 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("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("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')), 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)) diff --git a/src/ifcopenshell-python/ifcopenshell/test_ids.py b/src/ifcopenshell-python/ifcopenshell/test_ids.py new file mode 100644 index 0000000000..bec6587600 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/test_ids.py @@ -0,0 +1,148 @@ +import unittest +import ids +import requests +import os +# from xmlschema.validators.exceptions import XMLSchemaChildrenValidationError + + +def read_web_file(URL): + return requests.get(URL).text + + +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.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.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.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.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.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.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)) ) + + """ Saving parsed IDS to IDS.xml """ + + 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 + + +class TestIdsResults(unittest.TestCase): + pass + # TODO + + +if __name__ == "__main__": + unittest.main()