Merge pull request #1530 from atomczak/GSoC#45-IDS-checking

IDS - xmlschema, restrictions and requirements
This commit is contained in:
Thomas Krijnen
2021-07-10 15:35:08 +02:00
committed by GitHub
2 changed files with 572 additions and 97 deletions
+424 -97
View File
@@ -1,7 +1,12 @@
import operator import operator
import ifcopenshell.util.element 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): class exception(Exception):
@@ -46,22 +51,27 @@ class facet(metaclass=meta_facet):
The base class for IDS facets. IDS facets are functors constructed from 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 XML nodes that return True or False. A getattr method is provided for
conveniently extracting XML child node text content. conveniently extracting XML child node text content.
""" """
def __init__(self, node): def __init__(self, node=None, location=None):
self.node = node 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): def __getattr__(self, k):
try: if k in self.node:
v = self.node.getElementsByTagName(k)[0] v = self.node[k]
except IndexError: if isinstance(v, dict): #is restriction?
v = None return restriction(v['xs:restriction'][0])
if v:
elems = [n for n in v.childNodes if n.nodeType == n.ELEMENT_NODE]
if elems:
return restriction(elems[0])
else: else:
return v.firstChild.nodeValue.strip() return v
else: else:
return None return None
@@ -84,94 +94,248 @@ class entity(facet):
parameters = ["name", "predefinedtype"] 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): def __call__(self, inst, logger):
# @nb with inheritance # @nb with inheritance
if self.predefinedtype and hasattr(inst, "PredefinedType"): 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'" 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: else:
self.message = "an entity name '%(name)s'" 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): class classification(facet):
""" """
The IDS classification facet by traversing the HasAssociations inverse attribute The IDS classification facet by traversing the HasAssociations inverse attribute
""" """
parameters = ["system", "value"] parameters = ["system", "value", "location"]
message = "a classification reference '%(value)s' from '%(system)s'" 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): 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 = [] refs = []
for association in inst.HasAssociations: for association in associations:
if association.is_a("IfcRelAssociatesClassification"): if association.is_a("IfcRelAssociatesClassification"):
cref = association.RelatingClassification 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.location_msg = location[self.location]
(self.system, self.value) in refs,
# @todo if refs:
"[classification_eval_todo]", 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): 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() def create(location='any', propertyset=None, name=None, value=None):
message = "a property '%(name)s' in '%(propertyset)s' with value '%(value)s'" 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): 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) pset = props.get(self.propertyset)
val = pset.get(self.name) if pset else None 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 = { di = {
"name": self.name, "name": self.name,
"propertyset": self.propertyset, "propertyset": self.propertyset,
"value": val, "value": "'%s'" % val,
"location": self.location_msg
} }
if val is not None: if val is not None:
msg = self.message % di msg = self.message % di
else: else:
if pset: 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: 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): class material(facet):
""" """
The IDS material facet The IDS material facet by traversing the HasAssociations inverse attribute
""" """
parameters = ["name", "value"] parameters = ["value", "location"]
message = "a material '%(name)s with value '%(value)s'" 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): 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: for rel in material_relations:
# @todo not all subtypes of IfcMaterial handled if rel.RelatingMaterial.is_a() == "IfcMaterial":
if rel.RelatingMaterial.is_a() == "IfcMaterialLayerSetUsage": 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 layers = rel.RelatingMaterial.ForLayerSet.MaterialLayers
names = [layer.Material.Name for layer in layers] [materials.append(layer.Material.Name) for layer in layers]
elif rel.RelatingMaterial.is_a() == "IfcMaterial": elif rel.RelatingMaterial.is_a() == "IfcMaterialProfileSet":
names.append(rel.RelatingMaterial.Name) [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( return facet_evaluation(
0, self.value in materials,
# @todo self.message % {"value": "'/'".join(materials), "location": self.location_msg},
"[material_eval_todo]",
) )
@@ -186,7 +350,10 @@ class boolean_logic:
def __call__(self, *args): def __call__(self, *args):
eval = [t(*args) for t in self.terms] eval = [t(*args) for t in self.terms]
join = [" and ", " or "][self.fold == any] 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): def __str__(self):
return [" and ", " or "][self.fold == any].join(map(str, self.terms)) return [" and ", " or "][self.fold == any].join(map(str, self.terms))
@@ -207,37 +374,84 @@ class restriction:
def __init__(self, node): def __init__(self, node):
self.restriction_on = node.getAttribute("base") self.restriction_on = node['@base'][3:]
self.type = ""
self.options = [] self.options = []
self.type = []
for n in node:
for n in node.childNodes: if n[0:3] == "xs:":
if n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("enumeration"): if n[3:] == "enumeration":
self.options.append(n.getAttribute("value")) self.type = "enumeration"
self.type = "enumeration" for x in node[n]:
elif n.nodeType == n.ELEMENT_NODE and (n.tagName.endswith("Inclusive") or n.tagName.endswith("Exclusive")): self.options.append(x["@value"])
self.options.append(n.getAttribute("value")) elif n[8:] == "clusive":
self.type = "bounds" self.type = "bounds"
elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("length"): if n[3:6] == 'min':
self.options.append(n.getAttribute("value")) self.options.insert(0,'>')
self.type = "length" else:
elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"): self.options.insert(0,'<')
self.options.append(n.getAttribute("value")) if n[6:9] == 'Inc':
self.type = "pattern" 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): 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): def __repr__(self):
if self.type == "enumeration": if self.type == "enumeration":
return " or ".join(self.options) return "'%s'" % "' or '".join(self.options)
elif self.type == "bounds": elif self.type == "bounds":
self.options.sort() 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": 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": 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: class specification:
@@ -245,29 +459,81 @@ class specification:
Represents the XML <specification> node and its two children <applicability> and <requirements> Represents the XML <specification> node and its two children <applicability> and <requirements>
""" """
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): def parse_rules(node):
children = [n for n in node.childNodes if n.nodeType == n.ELEMENT_NODE] names = [req for req in node for n in node[req]]
names = map(operator.attrgetter("tagName"), children) children = [child for req in node for child in node[req]]
classes = map(meta_facet.facets.__getitem__, names) 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] # TODO adding applicability/requirements to specification. How to avoid repetitions?
def add_applicability(self, facet):
len(phrases) == 2 or error("expected two child nodes for <specification>") """
phrases[0].tagName == "applicability" or error("expected <applicability>") Applicability specifies what conditions must be meet for an IFC object to be used for validation.
phrases[1].tagName == "requirements" or error("expected <requirements>") Takes: entity, classification, property or material objects as an input (at least one entity is required).
"""
self.applicability, self.requirements = (boolean_and(parse_rules(phrase)) for phrase in phrases) 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): def __call__(self, inst, logger):
if self.applicability(inst, logger): if self.applicability(inst, logger):
valid = self.requirements(inst, logger) valid = self.requirements(inst, logger)
if valid: 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: 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): def __str__(self):
return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__
@@ -278,34 +544,95 @@ class ids:
Represents the XML root <ids> node and its <specification> childNodes. Represents the XML root <ids> node and its <specification> childNodes.
""" """
def __init__(self, fn): def __init__(self):
dom = parse(fn) self.specifications = []
ids = dom.childNodes[0] self.info = None
ids.tagName == "ids" or error("expected <ids>") #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('<?xml version="1.0" encoding="UTF-8"?>\n')
f.write('<!-- IDS (INFORMATION DELIVERY SPECIFICATION) CREATED USING IFCOPENSHELL -->\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): def validate(self, ifc_file, logger):
self.ifc_checked = 0
self.ifc_passed = 0
for spec in self.specifications: for spec in self.specifications:
for elem in ifc_file.by_type("IfcObject"): 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__": if __name__ == "__main__":
import time
start_time = time.time()
import sys, os import sys, os
import logging import logging
import ifcopenshell 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") logger = logging.getLogger("IDS")
logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s") logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s")
logging.FileHandler(filename, mode='w') logging.FileHandler(filename, mode='w')
ids_file = ids(sys.argv[1])
ifc_file = ifcopenshell.open(sys.argv[2]) ifc_file = ifcopenshell.open(sys.argv[2])
ids_file = ids.parse(sys.argv[1])
ids_file.validate(ifc_file, logger) 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)) 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))
@@ -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()