def _entrypoint_name(self, doc_concepts): """ Used to find the name of the correct entrypoint given a set of concepts from a document (JSON or XML). Currently assumes that a JSON/XML document contains one and only one entrypoint and raises an exception if this is not true. Args: doc_concepts (list of strings): A list of all concepts in the input JSON/XML document. Returns: A string contianing the name of the correct entrypoint. TODO: This is algorithm is fairly slow since it loops through all entry points which is time consuming for a small number of input concepts. With this said there is not currently enough support functions in Taxonomy to accomodate other algorithms. It may be better to move this into taxonomy_semantic and simultaneously improve the speed. """ entrypoints_found = set() for entrypoint in self._taxonomy.semantic.get_all_entrypoints(): for concept in self._taxonomy.semantic.get_entrypoint_concepts( entrypoint): for doc_concept in doc_concepts: if doc_concept == concept: entrypoints_found.add(entrypoint) if len(entrypoints_found) == 0: raise ob.OBValidationError( "No entrypoint found given the set of facts") elif len(entrypoints_found) == 1: for entrypoint in entrypoints_found: return entrypoint else: # Multiple candidate entrypoints are found. See if there is one and only one # perfect fit. the_one = None for entrypoint in entrypoints_found: ok = True for doc_concept in doc_concepts: ok2 = False for doc_concept2 in self._taxonomy.semantic.get_entrypoint_concepts( entrypoint): if doc_concept == doc_concept2: ok2 = True break if not ok2: ok = False break if ok: if the_one is None: the_one = entrypoint else: raise ob.OBValidationError( "Multiple entrypoints ({}, {}) found given the set of facts" .format(the_one, entrypoint)) if the_one == None: raise ob.OBValidationError( "No entrypoint found given the set of facts") else: return the_one
def test_ob_errors(self): self.assertIsInstance(ob.OBError("test message"), ob.OBError) self.assertIsInstance(ob.OBTypeError("test message"), ob.OBTypeError) self.assertIsInstance(ob.OBContextError("test message"), ob.OBContextError) self.assertIsInstance(ob.OBConceptError("test message"), ob.OBConceptError) self.assertIsInstance(ob.OBNotFoundError("test message"), ob.OBNotFoundError) self.assertIsInstance(ob.OBUnitError("test message"), ob.OBUnitError) self.assertIsInstance(ob.OBValidationError("test message"), ob.OBValidationError)
def validate_concept_value(self, concept_details, value): """ Validate a concept value. Args: concept_details (ConceptDetails): concept details. value (*): value to be validated. Returns: A tuple (*, list of str) containing original or converted value and list of errors (can be empty). """ errors = [] result = (value, []) # If null check if nillable is ok and return if value is None and not concept_details.nillable: errors += [ "'{}' is not allowed to be nillable (null).".format( concept_details.id) ] enum = self._taxonomy.types.get_type_enum(concept_details.type_name) # Check data type and validator calling if type(concept_details.type_name).__name__ in ["str", "unicode"]: method_name = self._get_validator_method_name( concept_details.type_name) # validator_module = sys.modules[__name__] found_method = getattr(self, method_name, None) if found_method: if enum: result = found_method(value, enum) else: result = found_method(value) elif enum: result = self._generic_enum_validator(value, concept_details, enum) else: raise ob.OBValidationError( "Concept '{}' could not be processed. Missing method '{}'." .format(concept_details.type_name, method_name)) # Check identifiers. This is based upon the name of the field containing # the word Identifier in it. Avoid UtilityIdentifier which is a LEI. if concept_details.id != "solar:UtilityIdentifier" and concept_details.id.find( "Identifier") != -1: if not identifier.validate(value): errors += [ "'{}' is not valid identifier.".format(concept_details.id) ] # If all conditions clear then the value passes. errors += result[1] return result[0], errors
def from_XML_string(self, xml_string, entrypoint_name=None): """ Loads the Entrypoint from an XML string. If no entrypoint_name is given the entrypoint will be derived from the facts. In some cases this is not possible because more than one entrypoint could exist given the list of facts and in these cases an entrypoint is required. Args: xml_string(str): String containing XML. entrypoint_name (str): Optional name of the entrypoint. Returns: OBInstance containing the loaded data. """ # NOTE: The XML parser has much less effort placed into both the coding and testing as # opposed to the coding and testing effort performed on the JSON parser. To some extent # this is on purpose since the hope is that the bulk of Orange Button data will be # transmitted using JSON. With this you are invited to (a) refactor the XML parsing code # and (b) create XML test cases if you believe that XML parser should receive the same # effort level as the JSON parser. # Create a validation error which can be used to maintain a list of error messages validation_errors = ob.OBValidationErrors( "Error(s) found in input JSON") try: root = ElementTree.fromstring(xml_string) except Exception as e: validation_errors.append(e) raise validation_errors # Read all elements that are not a context or a unit: if not entrypoint_name: fact_names = [] for child in root: if child.tag != _xn("link:schemaRef") and child.tag != _xn( "unit") and child.tag != _xn("context"): tag = child.tag tag = tag.replace( "{http://xbrl.us/Solar/v1.2/2018-03-31/solar}", "solar:") tag = tag.replace("{http://fasb.org/us-gaap/2017-01-31}", "us-gaap:") tag = tag.replace("{http://xbrl.sec.gov/dei/2014-01-31}", "dei:") fact_names.append(tag) try: entrypoint_name = self._entrypoint_name(fact_names) except ob.OBValidationError as ve: validation_errors.append(ve) raise validation_errors # Create an entrypoint. entrypoint = data_model.OBInstance(entrypoint_name, self._taxonomy, dev_validation_off=True) # Read in units units = {} for unit in root.iter(_xn("unit")): units[unit.attrib["id"]] = unit[0].text # Read in contexts contexts = {} for context in root.iter(_xn("context")): instant = None duration = None entity = None start_date = None end_date = None axis = {} for elem in context.iter(): if elem.tag == _xn("period"): if elem[0].tag == _xn("forever"): duration = "forever" elif elem[0].tag == _xn("startDate"): start_date = elem[0].text elif elem[0].tag == _xn("endDate"): end_date = elem[0].text elif elem[0].tag == _xn("instant"): instant = elem[0].text elif elem.tag == _xn("entity"): for elem2 in elem.iter(): if elem2.tag == _xn("identifier"): entity = elem2.text elif elem2.tag == _xn("segment"): for elem3 in elem2.iter(): if elem3.tag == _xn("xbrldi:typedMember"): for elem4 in elem3.iter(): if elem4.tag != _xn( "xbrldi:typedMember"): axis[elem3.attrib[ "dimension"]] = elem4.text if duration is None and start_date is not None and end_date is not None: duration = {"start": start_date, "end": end_date} kwargs = {} if instant is not None: kwargs["instant"] = instant if duration is not None: kwargs["duration"] = duration if entity is not None: kwargs["entity"] = entity if axis is not None: for a in axis: kwargs[a] = axis[a] if instant is None and duration is None: validation_errors.append( ob.OBValidationError( "Context is missing both a duration and instant tag")) if entity is None: validation_errors.append("Context is missing an entity tag") try: dm_ctx = data_model.Context(**kwargs) contexts[context.attrib["id"]] = dm_ctx except Exception as e: validation_errors.append(e) # Read all elements that are not a context or a unit: for child in root: if child.tag != _xn("link:schemaRef") and child.tag != _xn( "unit") and child.tag != _xn("context"): kwargs = {} fact_id = None if "id" in child.attrib: fact_id = child.attrib["id"] if "contextRef" in child.attrib: if child.attrib["contextRef"] in contexts: kwargs["context"] = contexts[ child.attrib["contextRef"]] kwargs["fact_id"] = fact_id tag = child.tag tag = tag.replace( "{http://xbrl.us/Solar/v1.2/2018-03-31/solar}", "solar:") tag = tag.replace( "{http://fasb.org/us-gaap/2017-01-31}", "us-gaap:") tag = tag.replace( "{http://xbrl.sec.gov/dei/2014-01-31}", "dei:") try: entrypoint.set(tag, child.text, **kwargs) except Exception as e: validation_errors.append(e) else: validation_errors.append( "referenced context is missing") else: validation_errors.append("Element is missing a context") # Raise the errors if necessary if validation_errors.get_errors(): raise validation_errors # Return populated entrypoint return entrypoint