ribes the actions and what they can do
        :return:
        """
        #description_list = []
        #for action in self.allowable_actions:
        #    description_list.append(getattr(self, '_' + self.__class__.__name__ + '__' + action)(describe=True))
        #return description_list
        return list(self.command_definitions.values())

    # Write a little helper function to test parameters
    def check_params(self, allowable_parameters):
        """
        Checks to see if the input parameters are allowed
        :param input_parameters: input parameters supplied to ARAXOverlay.apply()
        :param allowable_parameters: the allowable parameters
        :return: None
        """
        for key, item in self.parameters.items():
            if key not in allowable_parameters:
                self.response.error(
                    f"Supplied parameter {key} is not permitted. Allowable parameters are: {list(allowable_parameters.keys())}",
                    error_code="UnknownParameter")
                return -1
            elif type(item) == list or type(item) == set:
                for item_val in item:
                    if item_val not in allowable_parameters[key]:
                        self.response.warning(
                            f"Supplied value {item_val} is not permitted. In action {allowable_parameters['action']}, allowable values to {key} are: {list(allowable_parameters[key])}")
                        return -1
            elif item not in allowable_parameters[key]:
                if any([type(x) == float for x in allowable_parameters[key]]):  # if it's a float, just accept it as it is
                    continue
                else:  # otherwise, it's really not an allowable parameter
                    self.response.warning(
                        f"Supplied value {item} is not permitted. In action {allowable_parameters['action']}, allowable values to {key} are: {list(allowable_parameters[key])}")
                    return -1

    #### Top level decision maker for applying filters
    def apply(self, input_response, input_parameters):

        #### Define a default response
        #response = ARAXResponse()
        self.response = input_response
        self.message = input_response.envelope.message

        #### Basic checks on arguments
        if not isinstance(input_parameters, dict):
            self.response.error("Provided parameters is not a dict", error_code="ParametersNotDict")
            return self.response

        # list of actions that have so far been created for ARAX_overlay
        allowable_actions = self.allowable_actions

        # check to see if an action is actually provided
        if 'action' not in input_parameters:
            self.response.error(f"Must supply an action. Allowable actions are: action={allowable_actions}", error_code="MissingAction")
        elif input_parameters['action'] not in allowable_actions:
            self.response.error(f"Supplied action {input_parameters['action']} is not permitted. Allowable actions are: {allowable_actions}", error_code="UnknownAction")

        #### Return if any of the parameters generated an error (showing not just the first one)
        if self.response.status != 'OK':
            return self.response

        # populate the parameters dict
        parameters = dict()
        for key, value in input_parameters.items():
            parameters[key] = value

        #### Store these final parameters for convenience
        self.response.data['parameters'] = parameters
        self.parameters = parameters

        # convert the action string to a function call (so I don't need a ton of if statements
        getattr(self, '_' + self.__class__.__name__ + '__' + parameters['action'])()  # thank you https://stackoverflow.com/questions/11649848/call-methods-by-string

        self.response.debug(f"Applying Overlay to Message with parameters {parameters}")  # TODO: re-write this to be more specific about the actual action

        #### Return the response and done
        if self.report_stats:  # helper to report information in debug if class self.report_stats = True
            self.response = self.report_response_stats(self.response)
        return self.response

    def __sort_by_edge_attribute(self, describe=False):
        """
        sorts by results edge attribute
        :return:
        """
        message = self.message
        parameters = self.parameters
        # make a list of the allowable parameters (keys), and their possible values (values). Note that the action and corresponding name will always be in the allowable parameters
        if message and parameters and hasattr(message, 'results') and hasattr(message, 'knowledge_graph') and hasattr(message.knowledge_graph, 'edges'):
            known_attributes = set()
            virtual_relation_labels = set()
            for edge in message.knowledge_graph.edges.values():
                if hasattr(edge, 'attributes'):
                    if edge.attributes:
                        for attribute in edge.attributes:
                            if hasattr(attribute,"original_attribute_name"):
                                known_attributes.add(attribute.original_attribute_name)
                                if attribute.original_attribute_name == "virtual_relation_label":
                                    virtual_relation_labels.add(attribute.value)
                            if hasattr(attribute,"attribute_type_id"):
                                known_attributes.add(attribute.attribute_type_id)
            # print(known_attributes)
            allowable_parameters = {'action': {'sort_by_edge_attribute'},
                                    'edge_attribute': known_attributes,
                                    'edge_relation': virtual_relation_labels,
                                    'direction': {'descending', 'd', 'ascending', 'a'},
                                    'max_results': {float()},
                                    'prune_kg': {'true', 'false', 'True', 'False', 't', 'f', 'T', 'F'},
                                    'qedge_keys': set([t for x in self.message.knowledge_graph.edges.values() if x.qedge_keys is not None for t in x.qedge_keys])
                                    }
        else:
            allowable_parameters = {'action': {'sort_by_edge_attribute'},
                                    'edge_attribute': {'an edge attribute'},
                                    'edge_relation': {'an edge relation'},
                                    'direction': {'descending', 'd', 'ascending', 'a'},
                                    'max_results': {'the maximum number of results to return'},
                                    'prune_kg': {'true', 'false', 'True', 'False', 't', 'f', 'T', 'F'},
                                    'qedge_keys': {'A list of the specific qedge_keys you want to consider when filtering'}
                                    }

        # A little function to describe what this thing does
        if describe:
            brief_description = self.command_definitions['sort_by_edge_attribute']
            allowable_parameters['brief_description'] = brief_description
            return allowable_parameters

        edge_params = self.parameters


        if self.check_results(message):
            return self.response

        # try to convert the max results to an int
        if 'max_results' in edge_params:
            try:
                edge_params['max_results'] = int(edge_params['max_results'])
                assert edge_params['max_results'] >= 0
            except:
                tb = traceback.format_exc()
                error_type, error, _ = sys.exc_info()
                self.response.error(tb, error_code=error_type.__name__)
                self.response.error(f"parameter 'max_results' must be a non-negative integer")
            if self.response.status != 'OK':
                return self.response

        # Make sure only allowable parameters and values have been passed
        print(self.parameters)
        print(allowable_parameters)
        resp = self.check_params(allowable_parameters)
        # return if bad parameters have been passed
        if self.response.status != 'OK' or resp == -1:
            return self.response

        if 'direction' not in edge_params:
            self.response.error(
                f"Direction must be provided, allowable directions are: {list(allowable_parameters['direction'])}",
                error_code="UnknownValue")
        else:
            value = edge_params['direction']
            if value in {'descending', 'd'}:
                edge_params['descending'] = True
            elif value in {'ascending', 'a'}:
                edge_params['descending'] = False
            else:
                self.response.error(
                    f"Supplied value {value} is not permitted. In parameter direction, allowable values are: {list(allowable_parameters['direction'])}",
                    error_code="UnknownValue")
        if 'edge_attribute' not in edge_params:
            self.response.error(
                f"Edge attribute must be provided, allowable attributes are: {list(allowable_parameters['edge_attribute'])}",
                error_code="UnknownValue")
        if 'prune_kg' not in edge_params:
            edge_params['prune_kg'] = True
        elif edge_params['prune_kg'] in {'true', 'True', 't', 'T'}:
            edge_params['prune_kg'] = True
        elif edge_params['prune_kg'] in {'false', 'False', 'f', 'F'}:
            edge_params['prune_kg'] = False
        else:
            value = edge_params['prune_kg']
            self.response.error(
                    f"Supplied value {value} is not permitted. In parameter prune_kg, allowable values are: {list(allowable_parameters['prune_kg'])}",
                    error_code="UnknownValue")
        if self.response.status != 'OK':
            return self.response

        # now do the call out to NGD
        from Filter_Results.sort_results import SortResults
        SR = SortResults(self.response, self.message, edge_params)
        response = SR.sort_by_edge_attribute()
        return response

    def __sort_by_node_attribute(self, describe=False):
        """
        sorts results by node attribute
        :return:
        """
        message = self.message
        parameters = self.parameters
        # make a list of the allowable parameters (keys), and their possible values (values). Note that the action and corresponding name will always be in the allowable parameters
        if message and parameters and hasattr(message, 'results') and hasattr(message, 'knowledge_graph') and hasattr(message.knowledge_graph, 'nodes'):
            known_attributes = set()
            for node in message.knowledge_graph.nodes.values():
                if hasattr(node, 'attributes'):
                    if node.attributes:
                        for attribute in node.attributes:
                            if hasattr(attribute,"original_attribute_name"):
                                known_attributes.add(attribute.original_attribute_name)
                            if hasattr(attribute,"attribute_type_id"):
                                known_attributes.add(attribute.attribute_type_id)
            # print(known_attributes)
            allowable_parameters = {'action': {'sort_by_node_attribute'},
                                    'node_attribute': known_attributes,
                                    'node_category': set([t for x in self.message.knowledge_graph.nodes.values() for t in x.categories]),
                                    'direction': {'descending', 'd', 'ascending', 'a'},
                                    'max_results': {float()},
                                    'prune_kg': {'true', 'false', 'True', 'False', 't', 'f', 'T', 'F'},
                                    'qnode_keys': set([t for x in self.message.knowledge_graph.nodes.values() if x.qnode_keys is not None for t in x.qnode_keys])
                                    }
        else:
            allowable_parameters = {'action': {'sort_by_node_attribute'},
                                    'node_attribute': {'a node attribute'},
                                    'node_category': {'a node category'},
                                    'direction': {'descending', 'd', 'ascending', 'a'},
                                    'max_results': {'the maximum number of results to return'},
                                    'prune_kg': {'true', 'false', 'True', 'False', 't', 'f', 'T', 'F'},
                                    'qnode_keys': {'A list of the specific qnode_keys you wish to consider when sorting'}
                                    }

        # A little function to describe what this thing does
        if describe:
            # TODO: add more use cases
            brief_description = self.command_definitions['sort_by_node_attribute']
            allowable_parameters['brief_description'] = brief_description
            return allowable_parameters

        node_params = self.parameters

        if self.check_results(message):
            return self.response

        # try to convert the max results to an int
        if 'max_results' in node_params:
            try:
                node_params['max_results'] = int(node_params['max_results'])
                assert node_params['max_results'] >= 0
            except:
                tb = traceback.format_exc()
                error_type, error, _ = sys.exc_info()
                self.response.error(tb, error_code=error_type.__name__)
                self.response.error(f"parameter 'max_results' must be a non-negative integer")
            if self.response.status != 'OK':
                return self.response

        # Make sure only allowable parameters and values have been passed
        resp = self.check_params(allowable_parameters)
        # return if bad parameters have been passed
        if self.response.status != 'OK' or resp == -1:
            return self.response

        if 'direction' not in node_params:
            self.response.error(
                f"Direction must be provided, allowable directions are: {list(allowable_parameters['direction'])}",
                error_code="UnknownValue")
        else:
            value = node_params['direction']
            if value in {'descending', 'd'}:
                node_params['descending'] = True
            elif value in {'ascending', 'a'}:
                node_params['descending'] = False
            else:
                self.response.error(
                    f"Supplied value {value} is not permitted. In parameter direction, allowable values are: {list(allowable_parameters['direction'])}",
                    error_code="UnknownValue")
        if 'node_attribute' not in node_params:
            self.response.error(
                f"node attribute must be provided, allowable attributes are: {list(allowable_parameters['node_attribute'])}",
                error_code="UnknownValue")
        if 'prune_kg' not in node_params:
            node_params['prune_kg'] = True
        elif node_params['prune_kg'] in {'true', 'True', 't', 'T'}:
            node_params['prune_kg'] = True
        elif node_params['prune_kg'] in {'false', 'False', 'f', 'F'}:
            node_params['prune_kg'] = False
        else:
            value = node_params['prune_kg']
            self.response.error(
                    f"Supplied value {value} is not permitted. In parameter prune_kg, allowable values are: {list(allowable_parameters['prune_kg'])}",
                    error_code="UnknownValue")
        if self.response.status != 'OK':
            return self.response

        # now do the call out to NGD
        from Filter_Results.sort_results import SortResults
        SR = SortResults(self.response, self.message, node_params)
        response = SR.sort_by_node_attribute()
        return response


    def __limit_number_of_results(self, describe=False):
        """
        limits the number of results
        :return:
        """
        message = self.message
        parameters = self.parameters
        # make a list of the allowable parameters (keys), and their possible values (values). Note that the action and corresponding name will always be in the allowable parameters
        if message and parameters and hasattr(message, 'results'):
            allowable_parameters = {'action': {'limit_number_of_results'},
                                    'max_results': {float()},
                                    'prune_kg': {'true', 'false', 'True', 'False', 't', 'f', 'T', 'F'}
                                    }
        else:
            allowable_parameters = {'action': {'limit_number_of_results'},
                                    'max_results': {'a non-negative integer'},
                                    'prune_kg': {'true', 'false', 'True', 'False', 't', 'f', 'T', 'F'}
                                    }

        # A little function to describe what this thing does
        if describe:
            # TODO: add more use cases
            brief_description = self.command_definitions['limit_number_of_results']
            allowable_parameters['brief_description'] = brief_description
            return allowable_parameters

        params = self.parameters

        if self.check_results(message):
            return self.response


        # Make sure only allowable parameters and values have been passed
        resp = self.check_params(allowable_parameters)
        # return if bad parameters have been passed
        if self.response.status != 'OK' or resp == -1:
            return self.response

        # try to convert the max results to an int
        if 'max_results' in params:
            try:
                params['max_results'] = int(params['max_results'])
                assert params['max_results'] >= 0
            except:
                tb = traceback.format_exc()
                error_type, error, _ = sys.exc_info()
                self.response.error(tb, error_code=error_type.__name__)
                self.response.error(f"parameter 'max_results' must be a non-negative integer")
            if self.response.status != 'OK':
                return self.response
        else:
            self.response.error(
                f"Max results must be provided, allowable attributes are: {list(allowable_parameters['max_results'])}",
                error_code="UnknownValue")

        if 'prune_kg' not in params:
            params['prune_kg'] = True
        elif params['prune_kg'] in {'true', 'True', 't', 'T'}:
            params['prune_kg'] = True
        elif params['prune_kg'] in {'false', 'False', 'f', 'F'}:
            params['prune_kg'] = False
        else:
            value = params['prune_kg']
            self.response.error(
                    f"Supplied value {value} is not permitted. In parameter prune_kg, allowable values are: {list(allowable_parameters['prune_kg'])}",
                    error_code="UnknownValue")
        if self.response.status != 'OK':
            return self.response

        # now do the call out to NGD
        from Filter_Results.sort_results import SortResults
        SR = SortResults(self.response, self.message, params)
        response = SR.limit_number_of_results()
        return response

        
    def __sort_by_score(self, describe=False):
        """
        sorts by results scores
        :return:
        """
        message = self.message
        parameters = self.parameters
        # make a list of the allowable parameters (keys), and their possible values (values). Note that the action and corresponding name will always be in the allowable parameters
        if message and parameters and hasattr(message, 'results') and hasattr(message, 'knowledge_graph') and hasattr(message.knowledge_graph, 'edges'):
            allowable_parameters = {'action': {'sort_by_score'},
                                    'direction': {'descending', 'd', 'ascending', 'a'},
                                    'max_results': {float()},
                                    'prune_kg': {'true', 'false', 'True', 'False', 't', 'f', 'T', 'F'}
                                    }
        else:
            allowable_parameters = {'action': {'sort_by_score'},
                                    'direction': {'descending', 'd', 'ascending', 'a'},
                                    'max_results': {'the maximum number of results to return'},
                                    'prune_kg': {'true', 'false', 'True', 'False', 't', 'f', 'T', 'F'}
                                    }

        # A little function to describe what this thing does
        if describe:
            # TODO: add more use cases
            brief_description = self.command_definitions['sort_by_score']
            allowable_parameters['brief_description'] = brief_description
            return allowable_parameters

        edge_params = self.parameters

        if self.check_results(message):
            return self.response

        # try to convert the max results to an int
        if 'max_results' in edge_params:
            try:
                edge_params['max_results'] = int(edge_params['max_results'])
                assert edge_params['max_results'] >= 0
            except:
                tb = traceback.format_exc()
                error_type, error, _ = sys.exc_info()
                self.response.error(tb, error_code=error_type.__name__)
                self.response.error(f"parameter 'max_results' must be a non-negative integer")
            if self.response.status != 'OK':
                return self.response

        # Make sure only allowable parameters and values have been passed
        resp = self.check_params(allowable_parameters)
        # return if bad parameters have been passed
        if self.response.status != 'OK' or resp == -1:
            return self.response

        if 'direction' not in edge_params:
            self.response.error(
                f"Direction must be provided, allowable directions are: {list(allowable_parameters['direction'])}",
                error_code="UnknownValue")
        else:
            value = edge_params['direction']
            if value in {'descending', 'd'}:
                edge_params['descending'] = True
            elif value in {'ascending', 'a'}:
                edge_params['descending'] = False
            else:
                self.response.error(
                    f"Supplied value {value} is not permitted. In parameter direction, allowable values are: {list(allowable_parameters['direction'])}",
                    error_code="UnknownValue")
        if 'prune_kg' not in edge_params:
            edge_params['prune_kg'] = True
        elif edge_params['prune_kg'] in {'true', 'True', 't', 'T'}:
            edge_params['prune_kg'] = True
        elif edge_params['prune_kg'] in {'false', 'False', 'f', 'F'}:
            edge_params['prune_kg'] = False
        else:
            value = edge_params['prune_kg']
            self.response.error(
                    f"Supplied value {value} is not permitted. In parameter prune_kg, allowable values are: {list(allowable_parameters['prune_kg'])}",
                    error_code="UnknownValue")
        if self.response.status != 'OK':
            return self.response

        # now do the call out to NGD
        from Filter_Results.sort_results import SortResults
        SR = SortResults(self.response, self.message, edge_params)
        response = SR.sort_by_score()
        return response


    def __sort_by_edge_count(self, describe=False):
        """
        sorts by results edge attribute
        :return:
        """
        message = self.message
        parameters = self.parameters
        # make a list of the allowable parameters (keys), and their possible values (values). Note that the action and corresponding name will always be in the allowable parameters
        if message and parameters and hasattr(message, 'results') and hasattr(message, 'knowledge_graph') and hasattr(message.knowledge_graph, 'edges'):
            allowable_parameters = {'action': {'sort_by_edge_count'},
                                    'direction': {'descending', 'd', 'ascending', 'a'},
                                    'max_results': {float()},
                                    'prune_kg': {'true', 'false', 'True', 'False', 't', 'f', 'T', 'F'}
                                    }
        else:
            allowable_parameters = {'action': {'sort_by_edge_count'},
                                    'direction': {'descending', 'd', 'ascending', 'a'},
                                    'max_results': {'the maximum number of results to return'},
                                    'prune_kg': {'true', 'false', 'True', 'False', 't', 'f', 'T', 'F'}
                                    }

        # A little function to describe what this thing does
        if describe:
            # TODO: add more use cases
            brief_description = self.command_definitions['sort_by_edge_count']
            allowable_parameters['brief_description'] = brief_description
            return allowable_parameters

        edge_params = self.parameters

        if self.check_results(message):
            return self.response

        # try to convert the max results to an int
        if 'max_results' in edge_params:
            try:
                edge_params['max_results'] = int(edge_params['max_results'])
                assert edge_params['max_results'] >= 0
            except:
                tb = traceback.format_exc()
                error_type, error, _ = sys.exc_info()
                self.response.error(tb, error_code=error_type.__name__)
                self.response.error(f"parameter 'max_results' must be a non-negative integer")
            if self.response.status != 'OK':
                return self.response

        # Make sure only allowable parameters and values have been passed
        resp = self.check_params(allowable_parameters)
        # return if bad parameters have been passed
        if self.response.status != 'OK' or resp == -1:
            return self.response

        if 'direction' not in edge_params:
            self.response.error(
                f"Direction must be provided, allowable directions are: {list(allowable_parameters['direction'])}",
                error_code="UnknownValue")
        else:
            value = edge_params['direction']
            if value in {'descending', 'd'}:
                edge_params['descending'] = True
            elif value in {'ascending', 'a'}:
                edge_params['descending'] = False
            else:
                self.response.error(
                    f"Supplied value {value} is not permitted. In parameter direction, allowable values are: {list(allowable_parameters['direction'])}",
                    error_code="UnknownValue")
        if 'prune_kg' not in edge_params:
            edge_params['prune_kg'] = True
        elif edge_params['prune_kg'] in {'true', 'True', 't', 'T'}:
            edge_params['prune_kg'] = True
        elif edge_params['prune_kg'] in {'false', 'False', 'f', 'F'}:
            edge_params['prune_kg'] = False
        else:
            value = edge_params['prune_kg']
            self.response.error(
                    f"Supplied value {value} is not permitted. In parameter prune_kg, allowable values are: {list(allowable_parameters['prune_kg'])}",
                    error_code="UnknownValue")
        if self.response.status != 'OK':
            return self.response

        # now do the call out to NGD
        from Filter_Results.sort_results import SortResults
        SR = SortResults(self.response, self.message, edge_params)
        response = SR.sort_by_edge_count()
        return response

    def __sort_by_node_count(self, describe=False):
        """
        sorts by number of nodes in result
        :return:
        """
        message = self.message
        parameters = self.parameters
        # make a list of the allowable parameters (keys), and their possible values (values). Note that the action and corresponding name will always be in the allowable parameters
        if message and parameters and hasattr(message, 'results') and hasattr(message, 'knowledge_graph') and hasattr(message.knowledge_graph, 'nodes'):
            allowable_parameters = {'action': {'sort_by_node_count'},
                                    'direction': {'descending', 'd', 'ascending', 'a'},
                                    'max_results': {float()},
                                    'prune_kg': {'true', 'false', 'True', 'False', 't', 'f', 'T', 'F'}
                                    }
        else:
            allowable_parameters = {'action': {'sort_by_node_count'},
                                    'direction': {'descending', 'd', 'ascending', 'a'},
                                    'max_results': {'the maximum number of results to return'},
                                    'prune_kg': {'true', 'false', 'True', 'False', 't', 'f', 'T', 'F'}
                                    }

        # A little function to describe what this thing does
        if describe:
            # TODO: add more use cases
            brief_description = self.command_definitions['sort_by_node_count']
            allowable_parameters['brief_description'] = brief_description
            return allowable_parameters

        node_params = self.parameters

        if self.check_results(message):
            return self.response

        # try to convert the max results to an int
        if 'max_results' in node_params:
            try:
                node_params['max_results'] = int(node_params['max_results'])
                assert node_params['max_results'] >= 0
            except:
                tb = traceback.format_exc()
                error_type, error, _ = sys.exc_info()
                self.response.error(tb, error_code=error_type.__name__)
                self.response.error(f"parameter 'max_results' must be a non-negative integer")
            if self.response.status != 'OK':
                return self.response

        # Make sure only allowable parameters and values have been passed
        resp = self.check_params(allowable_parameters)
        # return if bad parameters have been passed
        if self.response.status != 'OK' or resp == -1:
            return self.response

        if 'direction' not in node_params:
            self.response.error(
                f"Direction must be provided, allowable directions are: {list(allowable_parameters['direction'])}",
                error_code="UnknownValue")
        else:
            value = node_params['direction']
            if value in {'descending', 'd'}:
                node_params['descending'] = True
            elif value in {'ascending', 'a'}:
                node_params['descending'] = False
            else:
                self.response.error(
                    f"Supplied value {value} is not permitted. In parameter direction, allowable values are: {list(allowable_parameters['direction'])}",
                    error_code="UnknownValue")
        if 'prune_kg' not in node_params:
            node_params['prune_kg'] = True
        elif node_params['prune_kg'] in {'true', 'True', 't', 'T'}:
            node_params['prune_kg'] = True
        elif node_params['prune_kg'] in {'false', 'False', 'f', 'F'}:
            node_params['prune_kg'] = False
        else:
            value = node_params['prune_kg']
            self.response.error(
                    f"Supplied value {value} is not permitted. In parameter prune_kg, allowable values are: {list(allowable_parameters['prune_kg'])}",
                    error_code="UnknownValue")
        if self.response.status != 'OK':
            return self.response

        # now do the call out to NGD
        from Filter_Results.sort_results import SortResults
        SR = SortResults(self.response, self.message, node_params)
        response = SR.sort_by_node_count()
        return response

##########################################################################################
def main():
    ### Note that most of this is just manually doing what ARAXQuery() would normally do for you

    #### Create a response object
    response = ARAXResponse()

    #### Create an ActionsParser object
    from actions_parser import ActionsParser
    actions_parser = ActionsParser()

    #### Set a simple list of actions
    # actions_list = [
    #    "overlay(compute_confidence_scores=true)",
    #    "return(message=true,store=false)"
    # ]

    actions_list = [
        #"filter_kg(action=remove_edges_by_predicate, edge_predicate=physically_interacts_with, remove_connected_nodes=false)",
        #"filter_kg(action=remove_edges_by_predicate, edge_predicate=physically_interacts_with, remove_connected_nodes=something)",
        #"filter(action=remove_nodes_by_category, node_category=protein)",
        #"overlay(action=compute_ngd)",
        #"filter(action=remove_edges_by_attribute, edge_attribute=ngd, threshold=.63, direction=below, remove_connected_nodes=t)",
        #"filter(action=remove_edges_by_attribute, edge_attribute=ngd, threshold=.6, remove_connected_nodes=False)",
        "filter(action=remove_orphaned_nodes)",
        "return(message=true,store=false)"
    ]

    #### Parse the action_list and print the result
    result = actions_parser.parse(actions_list)
    response.merge(result)
    if result.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response
    actions = result.data['actions']

    #### Read message #2 from the database. This should be the acetaminophen proteins query result message
    sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../../UI/Feedback")
    from RTXFeedback import RTXFeedback
    araxdb = RTXFeedback()

    #message_dict = araxdb.getMessage(2)  # acetaminophen2proteins graph
    # message_dict = araxdb.getMessage(13)  # ibuprofen -> proteins -> disease # work computer
    # message_dict = araxdb.getMessage(14)  # pleuropneumonia -> phenotypic_feature # work computer
    # message_dict = araxdb.getMessage(16)  # atherosclerosis -> phenotypic_feature  # work computer
    # message_dict = araxdb.getMessage(5)  # atherosclerosis -> phenotypic_feature  # home computer
    # message_dict = araxdb.getMessage(10)
    message_dict = araxdb.getMessage(40)

    #### The stored message comes back as a dict. Transform it to objects
    from ARAX_messenger import ARAXMessenger
    message = ARAXMessenger().from_dict(message_dict)
    # print(json.dumps(message.to_dict(),sort_keys=True,indent=2))

    #### Create an overlay object and use it to apply action[0] from the list
    #filterkg = ARAXFilterKG()
    #result = filterkg.apply(message, actions[0]['parameters'])
    #response.merge(result)

    # Apply overlay so you get an edge attribute to work with, then apply the filter
    #from ARAX_overlay import ARAXOverlay
    #overlay = ARAXOverlay()
    #result = overlay.apply(message, actions[0]['parameters'])
    #response.merge(result)
    # then apply the filter
    filterkg = ARAXFilterKG()
    result = filterkg.apply(message, actions[0]['parameters'])
    response.merge(result)

    # if result.status != 'OK':
    #    print(response.show(level=ARAXResponse.DEBUG))
    #    return response
    # response.data = result.data

    #### If successful, show the result
    # print(response.show(level=ARAXResponse.DEBUG))
    # response.data['message_stats'] = { 'n_results': message.n_results, 'id': message.id,
    #    'reasoner_id': message.reasoner_id, 'tool_version': message.tool_version }
    # response.data['message_stats']['confidence_scores'] = []
    # for result in message.results:
    #    response.data['message_stats']['confidence_scores'].append(result.confidence)

    # print(json.dumps(ast.literal_eval(repr(response.data['parameters'])),sort_keys=True,indent=2))
    # print(json.dumps(ast.literal_eval(repr(response.data['message_stats'])),sort_keys=True,indent=2))
    # a comment on the end so you can better see the network on github

    # look at the response
    # print(response.show(level=ARAXResponse.DEBUG))
    # print(response.show())
    # print("Still executed")

    # look at the edges
    # print(json.dumps(ast.literal_eval(repr(message.knowledge_graph.edges.values())),sort_keys=True,indent=2))
    # print(json.dumps(ast.literal_eval(repr(message.knowledge_graph.nodes.values())), sort_keys=True, indent=2))
    # print(json.dumps(message.to_dict(), sort_keys=True, indent=2))
    # print(response.show(level=ARAXResponse.DEBUG))

    # just print off the values
    # print(json.dumps(ast.literal_eval(repr(message.knowledge_graph.edges.values())), sort_keys=True, indent=2))
    # for edge in message.knowledge_graph.edges.values():
    #    if hasattr(edge, 'attributes') and edge.attributes and len(edge.attributes) >= 1:
    #        print(edge.attributes.pop().value)
    print(json.dumps(ast.literal_eval(repr(message.knowledge_graph.edges.values())), sort_keys=True, indent=2))
    print(response.show(level=ARAXResponse.DEBUG))
    vals = []
    for key, node in message.knowledge_graph.nodes.items():
        print(key)
    print(len(message.knowledge_graph.nodes))
    for edge in message.knowledge_graph.edges.values():
        if hasattr(edge, 'attributes') and edge.attributes and len(edge.attributes) >= 1:
            vals.append(edge.attributes.pop().value)
    print(sorted(vals))


if __name__ == "__main__": main()


# code/ARAX/ARAXQuery/ARAX_infer.py

#!/bin/env python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast
import re
import numpy as np
import math
from ARAX_response import ARAXResponse
from ARAX_messenger import ARAXMessenger
from ARAX_expander import ARAXExpander
from ARAX_resultify import ARAXResultify
from ARAX_decorator import ARAXDecorator
import traceback
from collections import Counter
from collections.abc import Hashable
from itertools import combinations
import copy

pathlist = os.path.realpath(__file__).split(os.path.sep)
RTXindex = pathlist.index("RTX")
sys.path.append(os.path.sep.join([*pathlist[:(RTXindex + 1)], 'code', 'UI', 'OpenAPI', 'python-flask-server']))
from openapi_server.models.q_edge import QEdge
from openapi_server.models.q_node import QNode
from openapi_server.models.attribute import Attribute as EdgeAttribute
from openapi_server.models.edge import Edge
from openapi_server.models.node import Node

sys.path.append(os.path.sep.join([*pathlist[:(RTXindex + 1)], 'code', 'ARAX', 'NodeSynonymizer']))
from node_synonymizer import NodeSynonymizer

sys.path.append(os.path.sep.join([*pathlist[:(RTXindex + 1)], 'code', 'ARAX', 'ARAXQuery', 'Infer', 'scripts']))
from infer_utilities import InferUtilities
# from creativeDTD import creativeDTD
from ExplianableDTD_db import ExplainableDTD

import pickle
import pandas as pd


class ARAXInfer:

    #### Constructor
    def __init__(self):
        self.kedge_global_iter = 0
        self.qedge_global_iter = 0
        self.qnode_global_iter = 0
        self.option_global_iter = 0
        self.response = None
        self.message = None
        self.parameters = None
        self.allowable_actions = {
            'drug_treatment_graph_expansion',
        }
        self.report_stats = True  # Set this to False when ready to go to production, this is only for debugging purposes

        #parameter descriptions
        self.node_curie_info = {
            "is_required": True,
            "examples": ["DOID:9352","MONDO:0005306","HP:0001945"],
            "type": "string",
            "description": "The curie for the node you wish to predict drugs which will treat."
        }
        self.qedge_id_info = {
            "is_required": False,
            "examples": ["qedge_id_1","qedge_id_2","qedge_id_3"],
            "type": "string",
            "description": "The id of the qedge you wish to perform the drug treatment inference expansion."
        }
        self.n_drugs_info = {
            "is_required": False,
            "examples": [5,50,100],
            "default": 50,
            "type": "integer",
            "description": "The number of drug nodes to return. If not provided defaults to 50."
        }
        self.n_paths_info = {
            "is_required": False,
            "examples": [5,50,100],
            "default": 20,
            "type": "integer",
            "description": "The number of paths connecting to each returned drug node. If not provided defaults to 20."
        }
        self.EDTD = ExplainableDTD()
        self.synonymizer = NodeSynonymizer()

        #command descriptions
        self.command_definitions = {
            "drug_treatment_graph_expansion": {
                "dsl_command": "infer(action=drug_treatment_graph_expansion)",
                "description": """
`drug_treatment_graph_expansion` predicts drug treatments for a given node curie. It return the top n results along with predicted graph explinations.  
            
You have the option to limit the maximum number of drug nodes to return (via `n_drugs=<n>`)
            
This can be applied to an arbitrary nide curie though will not return sensible results for non disease/phenotypic feature nodes.
                    """,
                'brief_description': """
drug_treatment_graph_expansion predicts drug treatments for a given node curie and provides along with an explination graph for each prediction.
                    """,
                "parameters": {
                    "node_curie": self.node_curie_info,
                    "qedge_id": self.qedge_id_info,
                    "n_drugs": self.n_drugs_info,
                    "n_paths": self.n_paths_info
                }
            }
        }


    # def __get_formated_edge_key(self, edge: Edge, kp: str = 'infores:rtx-kg2') -> str:
    #     return f"{kp}:{edge.subject}-{edge.predicate}-{edge.object}"

    def report_response_stats(self, response):
        """
        Little helper function that will report the KG, QG, and results stats to the debug in the process of executing actions. Basically to help diagnose problems
        """
        message = self.message
        if self.report_stats:
            # report number of nodes and edges, and their type in the QG
            if hasattr(message, 'query_graph') and message.query_graph:
                response.debug(f"Query graph is {message.query_graph}")
            if hasattr(message, 'knowledge_graph') and message.knowledge_graph and hasattr(message.knowledge_graph, 'nodes') and message.knowledge_graph.nodes and hasattr(message.knowledge_graph, 'edges') and message.knowledge_graph.edges:
                response.debug(f"Number of nodes in KG is {len(message.knowledge_graph.nodes)}")
                response.debug(f"Number of nodes in KG by type is {Counter([x.categories[0] for x in message.knowledge_graph.nodes.values()])}")  # type is a list, just get the first one
                #response.debug(f"Number of nodes in KG by with attributes are {Counter([x.category for x in message.knowledge_graph.nodes.values()])}")  # don't really need to worry about this now
                response.debug(f"Number of edges in KG is {len(message.knowledge_graph.edges)}")
                response.debug(f"Number of edges in KG by type is {Counter([x.predicate for x in message.knowledge_graph.edges.values()])}")
                response.debug(f"Number of edges in KG with attributes is {len([x for x in message.knowledge_graph.edges.values() if x.attributes])}")
                # Collect attribute names, could do this with list comprehension, but this is so much more readable
                attribute_names = []
                for x in message.knowledge_graph.edges.values():
                    if x.attributes:
                        for attr in x.attributes:
                            if hasattr(attr, "original_attribute_name"):
                                attribute_names.append(attr.original_attribute_name)
                            if hasattr(attr, "attribute_type_id"):
                                attribute_names.append(attr.attribute_type_id)      
                response.debug(f"Number of edges in KG by attribute {Counter(attribute_names)}")
        return response

    def describe_me(self):
        """
        Little helper function for internal use that describes the actions and what they can do
        :return:
        """
        #description_list = []
        #for action in self.allowable_actions:
        #    description_list.append(getattr(self, '_' + self.__class__.__name__ + '__' + action)(describe=True))
        #return description_list
        return list(self.command_definitions.values())

    # Write a little helper function to test parameters
    def check_params(self, allowable_parameters):
        """
        Checks to see if the input parameters are allowed
        :param input_parameters: input parameters supplied to ARAXOverlay.apply()
        :param allowable_parameters: the allowable parameters
        :return: None
        """
        for key, item in self.parameters.items():
            if key not in allowable_parameters:
                self.response.error(
                    f"Supplied parameter {key} is not permitted. Allowable parameters are: {list(allowable_parameters.keys())}",
                    error_code="UnknownParameter")
                return -1
            elif type(item) == list or type(item) == set:
                    for item_val in item:
                        if item_val not in allowable_parameters[key]:
                            self.response.error(
                                f"Supplied value {item_val} is not permitted. In action {allowable_parameters['action']}, allowable values to {key} are: {list(allowable_parameters[key])}")
                            return -1
            elif item not in allowable_parameters[key]:
                if any([type(x) == float for x in allowable_parameters[key]]):  # if it's a float, just accept it as it is
                    continue
                elif any([type(x) == int for x in allowable_parameters[key]]):
                    continue
                elif any([x is None for x in allowable_parameters[key]]):
                    continue
                elif key == "node_curie":  #FIXME: For now, if it's a node curie, just accept it as it is
                    continue
                else:  # otherwise, it's really not an allowable parameter
                    self.response.error(
                        f"This Supplied value {item} is not permitted. In action {allowable_parameters['action']}, allowable values to {key} are: {list(allowable_parameters[key])}")
                    return -1

    #### Top level decision maker for applying filters
    def apply(self, response, input_parameters):

        if response is None:
            response = ARAXResponse()
        self.response = response
        self.message = response.envelope.message

        #### Basic checks on arguments
        if not isinstance(input_parameters, dict):
            self.response.error("Provided parameters is not a dict", error_code="ParametersNotDict")
            return self.response

        # list of actions that have so far been created for ARAX_overlay
        allowable_actions = self.allowable_actions

        # check to see if an action is actually provided
        if 'action' not in input_parameters:
            self.response.error(f"Must supply an action. Allowable actions are: action={allowable_actions}", error_code="MissingAction")
        elif input_parameters['action'] not in allowable_actions:
            self.response.error(f"Supplied action {input_parameters['action']} is not permitted. Allowable actions are: {allowable_actions}", error_code="UnknownAction")

        #### Return if any of the parameters generated an error (showing not just the first one)
        if self.response.status != 'OK':
            return self.response

        # populate the parameters dict
        parameters = dict()
        for key, value in input_parameters.items():
            parameters[key] = value

        #### Store these final parameters for convenience
        self.response.data['parameters'] = parameters
        self.parameters = parameters

        # convert the action string to a function call (so I don't need a ton of if statements
        getattr(self, '_' + self.__class__.__name__ + '__' + parameters['action'])()  # thank you https://stackoverflow.com/questions/11649848/call-methods-by-string

        self.response.debug(f"Applying Infer to Message with parameters {parameters}")  # TODO: re-write this to be more specific about the actual action

        #### Return the response and done
        if self.report_stats:  # helper to report information in debug if class self.report_stats = True
            self.response = self.report_response_stats(self.response)
        return self.response

    def __drug_treatment_graph_expansion(self, describe=False):
        """
        Connects qnodes and runs expand.
        Allowable parameters: {'edge_predicate': str, 
                                'edge_property': str,
                                'direction': {'above', 'below'}}
        :return:
        """
        message = self.message
        parameters = self.parameters
        # make a list of the allowable parameters (keys), and their possible values (values). Note that the action and corresponding name will always be in the allowable parameters
        if message and parameters and hasattr(message, 'query_graph') and hasattr(message.query_graph, 'nodes'):
            allowable_parameters = {'action': {'drug_treatment_graph_expansion'},
                                    'node_curie': {str()},
                                    'qedge_id': set([key for key in self.message.query_graph.edges.keys()]),
                                    'n_drugs': {int()},
                                    'n_paths': {int()}
                                }
        else:
            allowable_parameters = {'action': {'drug_treatment_graph_expansion'},
                                    'node_curie': {'The node to predict drug treatments for.'},
                                    'qedge_id': {'The edge to place the predicted mechanism of action on. If none is provided, the query graph must be empty and a new one will be inserted.'},
                                    'n_drugs': {'The number of drugs to return. Defaults to 50.'},
                                    'n_paths': {'The number of paths connecting each drug to return. Defaults to 20.'}
                                }

        # A little function to describe what this thing does
        if describe:
            allowable_parameters['brief_description'] = self.command_definitions['connect_nodes']
            return allowable_parameters

        
        # Make sure only allowable parameters and values have been passed
        resp = self.check_params(allowable_parameters)
        # Make sure that if ARAXi node_curie is provided, that node is actually in the query graph
        if 'node_curie' in parameters and parameters['node_curie'] and hasattr(message, 'query_graph') and hasattr(message.query_graph, 'nodes') and message.query_graph.nodes:
            ids_in_qg = set()
            for node in message.query_graph.nodes.values():
                if node.ids:
                    ids_in_qg.update(node.ids)
            if parameters['node_curie'] not in ids_in_qg:
                self.response.error(f"Supplied node_curie {parameters['node_curie']} is not in the query graph. I was given the curies: {ids_in_qg}", error_code="UnknownNode")
                return self.response
        # return if bad parameters have been passed
        if self.response.status != 'OK' or resp == -1:
            return self.response


        # Set defaults and check parameters:
        if 'n_drugs' in self.parameters:
            if isinstance(self.parameters['n_drugs'], float):
                if self.parameters['n_drugs'].is_integer():
                    self.parameters['n_drugs'] = int(self.parameters['n_drugs'])
            if not isinstance(self.parameters['n_drugs'], int) or self.parameters['n_drugs'] < 1:
                self.response.error(
                f"The `n_drugs` value must be a positive integer. The provided value was {self.parameters['n_drugs']}.",
                error_code="ValueError")
        else:
            self.parameters['n_drugs'] = 50

        if 'n_paths' in self.parameters:
            if isinstance(self.parameters['n_paths'], float):
                if self.parameters['n_paths'].is_integer():
                    self.parameters['n_paths'] = int(self.parameters['n_paths'])
            if not isinstance(self.parameters['n_paths'], int) or self.parameters['n_paths'] < 1:
                self.response.error(
                f"The `n_paths` value must be a positive integer. The provided value was {self.parameters['n_paths']}.",
                error_code="ValueError")
        else:
            self.parameters['n_paths'] = 20

        if self.response.status != 'OK':
            return self.response


        # dtd = creativeDTD(data_path, model_path, use_gpu=False)

        # dtd.set_query_disease(self.parameters['node_curie'])
        # top_drugs = dtd.predict_top_N_drugs(self.parameters['n_drugs'])
        # top_paths = dtd.predict_top_M_paths(self.parameters['n_paths'])
        normalized_curie = self.synonymizer.get_canonical_curies(self.parameters['node_curie'])[self.parameters['node_curie']]
        if normalized_curie:
            preferred_curie = normalized_curie['preferred_curie']
            self.response.debug(f"Get a preferred sysnonym {preferred_curie} from Node Synonymizer for {self.parameters['node_curie']}")
        else:
            self.response.warning(f"Could not get a preferred sysnonym for disease {self.parameters['node_curie']}")

        try:
            top_drugs = self.EDTD.get_top_drugs_for_disease(disease_ids=preferred_curie)
            top_paths = self.EDTD.get_top_paths_for_disease(disease_ids=preferred_curie)
        except:
            self.response.error(f"Could not get top drugs and paths for disease {preferred_curie}", error_code="ValueError")
            return self.response

        if len(top_drugs) == 0:
            self.response.error(f"Could not get predicted drugs for disease {preferred_curie}. Likely the model was not trained with this disease.", error_code="ValueError")
            return self.response
        if len(top_paths) == 0:
            self.response.warning(f"Could not get any predicted paths for disease {preferred_curie}. Likely the model considers there is no reasonable path for this disease.")

        # FW: temp fix to use the pickle fil for dev work rather than recomputing
        # Comment out the following 3 lines and uncomment the above for prod deploy
        # top_drugs = pd.read_csv(os.path.sep.join([*pathlist[:(RTXindex + 1)], 'code', 'ARAX', 'ARAXQuery', 'Infer', 'data',"top_n_drugs.csv"]))
        # with open(os.path.sep.join([*pathlist[:(RTXindex + 1)], 'code', 'ARAX', 'ARAXQuery', 'Infer', 'data',"result_from_self_predict_top_M_paths.pkl"]),"rb") as fid:
        #     top_paths = pickle.load(fid)
        
        # TRAPI-ifies the results of the model
        iu = InferUtilities()
        qedge_id = self.parameters.get('qedge_id')

        self.response, self.kedge_global_iter, self.qedge_global_iter, self.qnode_global_iter, self.option_global_iter = iu.genrete_treat_subgraphs(self.response, top_drugs, top_paths, qedge_id, self.kedge_global_iter, self.qedge_global_iter, self.qnode_global_iter, self.option_global_iter)

        return self.response







##########################################################################################
def main():
    pass


if __name__ == "__main__": main()


# code/ARAX/ARAXQuery/ARAX_messenger.py

#!/bin/env python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, flush=True, **kwargs)

import os
import json
import ast
import re
import copy

from datetime import datetime
import numpy as np
import requests

from ARAX_response import ARAXResponse
from ARAX_resultify import ARAXResultify
from query_graph_info import QueryGraphInfo
from knowledge_graph_info import KnowledgeGraphInfo

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../")
from RTXConfiguration import RTXConfiguration

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../UI/OpenAPI/python-flask-server/")
from openapi_server.models.response import Response
from openapi_server.models.message import Message
from openapi_server.models.knowledge_graph import KnowledgeGraph
from openapi_server.models.query_graph import QueryGraph
from openapi_server.models.q_node import QNode
from openapi_server.models.q_edge import QEdge

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../NodeSynonymizer")
from node_synonymizer import NodeSynonymizer


class ARAXMessenger:

    #### Constructor
    def __init__(self):
        self.response = None
        self.envelope = None
        self.message = None
        self.parameters = None

    def describe_me(self):
        """
        Self-documentation method for internal use that returns the available actions and what they can do
        :return: A list of allowable actions supported by this class
        :rtype: list
        """
        description_list = []
        description_list.append(self.create_envelope(0,describe=True))
        description_list.append(self.add_qnode(0,0,describe=True))
        description_list.append(self.add_qedge(0,0,describe=True))
        return description_list


    # #### Create a fresh ARAXResponse object as an envelple and fill with defaults
    def create_envelope(self, response, describe=False):
        """
        Creates a basic empty ARAXResponse object with basic boilerplate metadata
        :return: ARAXResponse object with execution information and the new message object inside the data envelope
        :rtype: ARAXResponse
        """

        # #### Command definition for autogenerated documentation
        command_definition = {
            'dsl_command': 'create_envelope()',
            'description': """The `create_envelope` command creates a basic empty Response object with basic boilerplate metadata
                such as reasoner_id, schema_version, etc. filled in. This DSL command takes no arguments. This command is not explicitly
                necessary, as it is called implicitly when needed. e.g. If a DSL program begins with add_qnode(), the
                create_envelope() will be executed automatically if there is not yet a ARAXResponse. If there is already ARAXResponse in memory,
                then this command will destroy the previous one (in memory) and begin a new envelope.""",
            'parameters': {
            }
        }

        if describe:
            return command_definition


        #### Store the passed response object
        self.response = response
    
        #### Create the top-level Response object called an envelope
        response.info("Creating an empty template TRAPI Response")
        envelope = Response()
        response.envelope = envelope
        self.envelope = envelope

        # Create a Message object and place it in the envelope
        message = Message()
        response.envelope.message = message
        self.message = message

        #### Fill it with default information
        envelope.id = None
        envelope.type = "translator_reasoner_response"
        envelope.reasoner_id = "ARAX"
        envelope.tool_version = RTXConfiguration().version
        envelope.schema_version = "1.3.0"
        envelope.status = "OK"
        envelope.description = "Created empty template response"
        envelope.context = "https://raw.githubusercontent.com/biolink/biolink-model/master/context.jsonld"
        envelope.logs = response.messages
        envelope.datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

		#### Create an empty master knowledge graph
        message.knowledge_graph = KnowledgeGraph()
        message.knowledge_graph.nodes = {}
        message.knowledge_graph.edges = {}

		#### Create an empty query graph
        message.query_graph = QueryGraph()
        message.query_graph.nodes = {}
        message.query_graph.edges = {}

        #### Create empty results
        message.results = []

        #### Return the response
        response.data['envelope'] = envelope
        return response


    ###############################################################################################
    # #### Add a new QNode
    def add_qnode(self, response, input_parameters, describe=False):
        """
        Adds a new QNode object to the QueryGraph inside the Message object
        :return: ARAXResponse object with execution information
        :rtype: ARAXResponse
        """

        # #### Command definition for autogenerated documentation
        command_definition = {
            'dsl_command': 'add_qnode()',
            'description': """The `add_qnode` method adds an additional QNode to the QueryGraph in the Message object.""",
            'parameters': {
                'key': { 
                    'is_required': False,
                    'examples': [ 'n00', 'n01' ],
                    'default': '',
                    'type': 'string',
                    'description': """Any string that is unique among all QNode key fields, with recommended format n00, n01, n02, etc.
                        If no value is provided, autoincrementing values beginning for n00 are used.""",
                    },
                'ids': {
                    'is_required': False,
                    'examples': [ 'DOID:9281', '[UniProtKB:P12345,UniProtKB:Q54321]' ],
                    'type': 'string',
                    'description': 'A list (n >= 1) of compact URI (CURIE) (e.g. [DOID:9281] or [UniProtKB:P12345,UniProtKB:Q54321])',
                    },
                'name': {
                    'is_required': False,
                    'examples': [ 'hypertension', 'insulin' ],
                    'type': 'string',
                    'description': 'Any name of a bioentity that will be resolved into a CURIE if possible or result in an error if not (e.g. hypertension, insulin)',
                    },
                'categories': {
                    'is_required': False,
                    'examples': [ 'protein', 'chemical_substance', 'disease' ],
                    'type': 'ARAXnode',
                    'description': 'A list (n >= 1) of valid BioLink bioentity categories (e.g. biolink:Protein, biolink:ChemicalEntity, biolink:Disease)',
                    },
                'is_set': {
                    'is_required': False,
                    'enum': [ "true", "false", "True", "False", "t", "f", "T", "F" ],
                    'examples': [ 'true', 'false' ],
                    'type': 'boolean',
                    'description': 'If set to true, this QNode represents a set of nodes that are all in common between the two other linked QNodes (assumed to be false if not specified or value is not recognized as true/t case insensitive)'
                    },
                'option_group_id': {
                    'is_required': False,
                    'examples': [ '1', 'a', 'b2', 'option'],
                    'type': 'string',
                    'description': 'A group identifier indicating a group of nodes and edges should either all be included or all excluded. An optional match for all elements in this group. If not included Node will be treated as required.'
                    },
            }
        }

        if describe:
            return command_definition

        #### Extract the message to work on
        message = response.envelope.message
        self.response = response
        self.envelope = response.envelope
        self.messsage = message

        #### Basic checks on arguments
        if not isinstance(input_parameters, dict):
            response.error("Provided parameters is not a dict", error_code="ParametersNotDict")
            return response

        #### Define a complete set of allowed parameters and their defaults
        parameters = {
            'key': None,
            'ids': None,
            'name': None,
            'categories': None,
            'is_set': None,
            'option_group_id': None,
        }

        #### Loop through the input_parameters and override the defaults and make sure they are allowed
        for key,value in input_parameters.items():
            if key not in parameters:
                response.error(f"Supplied parameter {key} is not permitted", error_code="UnknownParameter")
            else:
                parameters[key] = value

        #### Check for option_group_id and is_set:
        if parameters['option_group_id'] is not None and parameters['ids'] is None and parameters['name'] is None:
            if parameters['is_set'] is None:
                parameters['is_set'] = 'true'
                response.warning(f"An 'option_group_id' was set to {parameters['option_group_id']}, but 'is_set' was not an included parameter. It must be true when an 'option_group_id' is given, so automatically setting to true. Avoid this warning by explictly setting to true.")
            elif not ( parameters['is_set'].lower() == 'true' or parameters['is_set'].lower() == 't' ):
                response.error(f"When an 'option_group_id' is given 'is_set' must be set to true. However, supplied input for parameter 'is_set' was {parameters['is_set']}.", error_code="InputMismatch")


        #### Return if any of the parameters generated an error (showing not just the first one)
        if response.status != 'OK':
            return response


        #### Now apply the filters. Order of operations is probably quite important
        #### Scalar value filters probably come first like minimum_confidence, then complex logic filters
        #### based on edge or node properties, and then finally maximum_results
        response.info(f"Adding a QueryNode to Message with input parameters {parameters}")

        #### Make sure there's a query_graph already here
        if message.query_graph is None:
            message.query_graph = QueryGraph()
            message.query_graph.nodes = {}
            message.query_graph.edges = {}
        if message.query_graph.nodes is None:
            message.query_graph.nodes = {}

        #### Set up the NodeSynonymizer to find curies and names
        synonymizer = NodeSynonymizer()

        # Create the QNode and set the key
        qnode = QNode()
        if parameters['key'] is not None:
            key = parameters['key']
            if key in message.query_graph.nodes:
                response.error(f"Duplicate key '{key}' specified when trying to create a new QNode", error_code="QNodeDuplicateKey")
                return response
        else:
            key = self.__get_next_free_node_key()

        if parameters['option_group_id'] is not None:
            qnode.option_group_id = parameters['option_group_id']
        
        # Set the is_set parameter to what the user selected
        if parameters['is_set'] is not None:
            qnode.is_set = ( parameters['is_set'].lower() == 'true' or parameters['is_set'].lower() == 't' )

        #### If the ids is specified, try to find that
        if parameters['ids'] is not None:

            # If the ids is a scalar then treat it here as a list of one
            if isinstance(parameters['ids'], str):
                id_list = [ parameters['ids'] ]

            # Or else set it up as a list
            elif isinstance(parameters['ids'], list):
                id_list = parameters['ids']
                is_id_a_list = True
                if len(id_list) == 1:
                    if qnode.is_set == True:
                        response.warning(f"Specified ids '{parameters['ids']}' is singular, but is_set=true, which doesn't make sense, so automatically setting to false. Avoid this warning by not explictly setting to true.")
                        qnode.is_set = False

            # Or if it's neither a list or a string, then error out. This cannot be handled at present
            else:
                response.error(f"Specified ids '{parameters['ids']}' is neither a string nor a list. This cannot to handled", error_code="IdNotListOrScalar")
                return response

            # Check whether NodeSynonymizer recognizes all IDs on this qnode
            response.debug(f"Looking up ids {id_list} in NodeSynonymizer")
            synonymizer_results = synonymizer.get_canonical_curies(id_list)
            unrecognized_ids = {synonymizer_result for synonymizer_result in synonymizer_results
                                if not synonymizer_result}
            if unrecognized_ids:
                response.debug(f"NodeSynonymizer did not recognize nodes {unrecognized_ids}, but will continue with them")
            qnode.ids = id_list

            if 'categories' in parameters and parameters['categories'] is not None:
                if isinstance(parameters['categories'], str):
                    qnode.categories = [ parameters['categories'] ]
                else:
                    qnode.categories = parameters['categories']

            message.query_graph.nodes[key] = qnode
            return response

        #### If the name is specified, try to find that
        if parameters['name'] is not None:
            name = parameters['name']
            response.debug(f"Looking up id for name '{name}' in NodeSynonymizer")
            synonymizer_results = synonymizer.get_canonical_curies(curies=[name], names=[name])

            if synonymizer_results[name] is None:
                response.error(f"A node with name '{name}' is not in our knowledge graph", error_code="UnresolvableNodeName")
                return response
 
            qnode.ids = [ synonymizer_results[name]['preferred_curie'] ]
            response.info(f"Creating QueryNode with ids '{qnode.ids}' for name '{name}'")
            if parameters['categories'] is not None:
                if isinstance(parameters['categories'], str):
                    qnode.categories = [ parameters['categories'] ]
                else:
                    qnode.categories = parameters['categories']
            message.query_graph.nodes[key] = qnode
            return response

        #### If the categories is specified, just add that categories. There should be checking that it is legal. FIXME
        if parameters['categories'] is not None:
            if isinstance(parameters['categories'], str):
                qnode.categories = [ parameters['categories'] ]
            else:
                qnode.categories = parameters['categories']
            if parameters['is_set'] is not None:
                qnode.is_set = (parameters['is_set'].lower() == 'true')
            message.query_graph.nodes[key] = qnode
            return response

        #### If we get here, it means that all three main parameters are null. Just a generic node with no categories or anything. This is okay.
        message.query_graph.nodes[key] = qnode
        return response


    ###############################################################################################
    #### Get the next free node key like nXX where XX is a zero-padded integer starting with 00
    def __get_next_free_node_key(self):

        #### Set up local references to the message and verify the query_graph nodes
        message = self.envelope.message
        if message.query_graph is None:
            message.query_graph = QueryGraph()
            message.query_graph.nodes = []
            message.query_graph.edges = []
        if message.query_graph.nodes is None:
            message.query_graph.nodes = []
        qnodes = message.query_graph.nodes

        #### Find the first unused key
        index = 0
        while 1:
            pad = '0'
            if index > 9:
                pad = ''
            potential_node_key = f"n{pad}{str(index)}"
            if potential_node_key not in qnodes:
                return potential_node_key
            index += 1


    ###############################################################################################
    #### Add a new QEdge
    def add_qedge(self, response, input_parameters, describe=False):
        """
        Adds a new QEdge object to the QueryGraph inside the Message object
        :return: ARAXResponse object with execution information
        :rtype: ARAXResponse
        """

        # #### Command definition for autogenerated documentation
        command_definition = {
            'dsl_command': 'add_qedge()',
            'description': """The `add_qedge` command adds an additional QEdge to the QueryGraph in the Message object. Currently
                subject and object QNodes must already be present in the QueryGraph. The specified type is not currently checked that it is a
                valid Translator/BioLink relationship type, but it should be.""",
            'parameters': {
                'key': { 
                    'is_required': False,
                    'examples': [ 'e00', 'e01' ],
                    'default': '',
                    'type': 'string',
                    'description': """Any string that is unique among all QEdge key fields, with recommended format e00, e01, e02, etc.
                        If no value is provided, autoincrementing values beginning for e00 are used.""",
                    },
                'subject': {
                    'is_required': True,
                    'examples': [ 'n00', 'n01' ],
                    'type': 'string',
                    'description': 'key of the source QNode already present in the QueryGraph (e.g. n00, n01)',
                    },
                'object': {
                    'is_required': True,
                    'examples': [ 'n01', 'n02' ],
                    'type': 'string',
                    'description': 'key of the target QNode already present in the QueryGraph (e.g. n01, n02)',
                    },
                'predicates': {
                    'is_required': False,
                    'examples': [ [ 'biolink:physically_interacts_with' ], [ 'biolink:participates_in' ] ],
                    'type': 'ARAXedge',
                    'description': 'A list (n >= 1) of valid BioLink relationship predicates (e.g. [physically_interacts_with], [participates_in])',
                    },
                'option_group_id': {
                    'is_required': False,
                    'examples': [ '1', 'a', 'b2', 'option'],
                    'type': 'string',
                    'description': 'A group identifier indicating a group of nodes and edges should either all be included or all excluded. An optional match for all elements in this group. If not included Node will be treated as required.'
                    },
                'exclude': {
                    'is_required': False,
                    'enum': [ 'true', 'false' ],
                    'examples': [ 'true', 'false' ],
                    'type': 'boolean',
                    'description': 'If set to true, results with this node will be excluded. If set to false or not included nodes will be treated as part of a normal query.'
                    },
            }
        }

        if describe:
            return command_definition


        #### Extract the message to work on
        message = response.envelope.message
        self.response = response
        self.envelope = response.envelope
        self.messsage = message

        #### Basic checks on arguments
        if not isinstance(input_parameters, dict):
            response.error("Provided parameters is not a dict", error_code="ParametersNotDict")
            return response

        #### Define a complete set of allowed parameters and their defaults
        parameters = {
            'key': None,
            'subject': None,
            'object': None,
            'predicates': None,
            'option_group_id': None,
            'exclude': None,
        }

        #### Loop through the input_parameters and override the defaults and make sure they are allowed
        for key,value in input_parameters.items():
            if key not in parameters:
                response.error(f"Supplied parameter {key} is not permitted", error_code="UnknownParameter")
            else:
                parameters[key] = value
        #### Return if any of the parameters generated an error (showing not just the first one)
        if response.status != 'OK':
            return response

        #### Store these final parameters for convenience
        response.data['parameters'] = parameters
        self.parameters = parameters


        #### Now apply the filters. Order of operations is probably quite important
        #### Scalar value filters probably come first like minimum_confidence, then complex logic filters
        #### based on edge or node properties, and then finally maximum_results
        response.info(f"Adding a QueryEdge to Message with parameters {parameters}")

        #### Make sure there's a query_graph already here
        if message.query_graph is None:
            message.query_graph = QueryGraph()
            message.query_graph.nodes = {}
            message.query_graph.edges = {}
        if message.query_graph.edges is None:
            message.query_graph.edges = {}

        #### Create a QEdge
        qedge = QEdge()
        if parameters['key'] is not None:
            key = parameters['key']
            if key in message.query_graph.edges:
                response.error(f"Duplicate key '{key}' specified when trying to create a new QEdge", error_code="QEdgeDuplicateKey")
                return response
        else:
            key = self.__get_next_free_edge_key()

        #### Get the list of available node_keys
        qnodes = message.query_graph.nodes

        #### Add the subject
        if parameters['subject'] is not None:
            if parameters['subject'] not in qnodes:
                response.error(f"While trying to add QEdge, there is no QNode with key {parameters['subject']}", error_code="UnknownSourceKey")
                return response
            qedge.subject = parameters['subject']
        else:
            response.error(f"While trying to add QEdge, subject is a required parameter", error_code="MissingSourceKey")
            return response

        #### Add the object
        if parameters['object'] is not None:
            if parameters['object'] not in qnodes:
                response.error(f"While trying to add QEdge, there is no QNode with key {parameters['object']}", error_code="UnknownTargetKey")
                return response
            qedge.object = parameters['object']
        else:
            response.error(f"While trying to add QEdge, object is a required parameter", error_code="MissingTargetKey")
            return response

        #### Add the predicates if any. Need to verify they are allowed predicates. FIXME
        if parameters['predicates'] is not None:
            if isinstance(parameters['predicates'], str):
                qedge.predicates = [ parameters['predicates'] ]
            else:
                qedge.predicates = parameters['predicates']

        if parameters['exclude'] is not None:
            if parameters['exclude'] in {'t', 'T', 'true', 'True'}:
                qedge.exclude = True
            elif parameters['exclude'] in {'f', 'F', 'false', 'False'}:
                qedge.exclude = False
            elif parameters['exclude'] not in {True, False}:
                response.error(f"Supplied input, {parameters['exclude']}, for the 'exclude' parameter is not valid. Acceptable inputs are t, T, f, F, true, True, false, and False.", error_code="UnknownInput")
        else:
            qedge.exclude = False

        if parameters['option_group_id'] is not None:
            qedge.option_group_id = parameters['option_group_id']

        #### Add it to the query_graph edge list
        message.query_graph.edges[key] = qedge

        #### Return the response
        return response


    ###############################################################################################
    #### Get the next free edge key like eXX where XX is a zero-padded integer starting with 00
    def __get_next_free_edge_key(self):

        #### Set up local references to the message and verify the query_graph nodes
        message = self.envelope.message
        if message.query_graph is None:
            message.query_graph = QueryGraph()
            message.query_graph.nodes = {}
            message.query_graph.edges = {}
        if message.query_graph.edges is None:
            message.query_graph.edges = {}
        qedges = message.query_graph.edges

        #### Find the first unused key
        index = 0
        while 1:
            pad = '0'
            if index > 9:
                pad = ''
            potential_edge_key = f"e{pad}{str(index)}"
            if potential_edge_key not in qedges:
                return potential_edge_key
            index += 1


    ###############################################################################################
    #### Remove a QEdge
    def remove_qedge(self, response, input_parameters, describe=False):
        """
        Removes a QEdge object in the QueryGraph inside the Message object
        :return: ARAXResponse object with execution information
        :rtype: ARAXResponse
        """

        # #### Command definition for autogenerated documentation
        command_definition = {
            'dsl_command': 'remove_qedge()',
            'description': """The `remove_qedge` command removes a QEdge from the QueryGraph in the Message object. Currently
                the only way to specify the desired edge to remove it by its key.""",
            'parameters': {
                'key': { 
                    'is_required': True,
                    'examples': [ 'e00', 'e01' ],
                    'default': '',
                    'type': 'string',
                    'description': """The key of the QEdge to remove, such as e00, e01, e02, etc.""",
                    },
           }
        }

        if describe:
            return command_definition


        #### Extract the message to work on
        message = response.envelope.message

        #### Basic checks on arguments
        if not isinstance(input_parameters, dict):
            response.error("Provided parameters is not a dict", error_code="ParametersNotDict")
            return response

        #### Define a complete set of allowed parameters and their defaults
        parameters = {
            'key': None,
        }

        #### Loop through the input_parameters and override the defaults and make sure they are allowed
        for key,value in input_parameters.items():
            if key not in parameters:
                response.error(f"Supplied parameter {key} is not permitted", error_code="UnknownParameter")
            else:
                parameters[key] = value
        #### Return if any of the parameters generated an error (showing not just the first one)
        if response.status != 'OK':
            return response

        #### Store these final parameters for convenience
        response.data['parameters'] = parameters
        self.parameters = parameters


        #### Now apply the filters. Order of operations is probably quite important
        #### Scalar value filters probably come first like minimum_confidence, then complex logic filters
        #### based on edge or node properties, and then finally maximum_results
        response.info(f"Removing QueryEdge with parameters {parameters}")

        #### Make sure there's a query_graph already here
        if message.query_graph is None:
            response.error(f"While trying to remove a QEdge, there is no QueryGraph", error_code="QueryGraphNotFound")
            return response
        if message.query_graph.edges is None:
            response.error(f"While trying to remove a QEdge, there are no edges in the QueryGraph", error_code="NoEdgesFound")
            return response

        key = parameters['key']
        if key in message.query_graph.edges:
            del message.query_graph.edges[key]
        else:
            response.error(f"While trying to remove a QEdge, no QEdge with key {key} was found", error_code="QEdgekeyNotfound")
            return response

        #### Return the response
        return response


    ###############################################################################################
    #### Fetch a message by its URI, return the message
    def fetch_message(self, message_uri):

        result = self.apply_fetch_message(self.message, { 'uri': message_uri })
        return self.message


    #### Fetch a message by its URI, return a full response
    def apply_fetch_message(self, message, input_parameters, describe=False):
        """
        Adds a new QEdge object to the QueryGraph inside the Message object
        :return: ARAXResponse object with execution information
        :rtype: ARAXResponse
        """

        # #### Command definition for autogenerated documentation
        command_definition = {
            'dsl_command': 'fetch_message()',
            'description': """The `fetch_message` command fetches a remote Message by its id and can then allow further processing on it.""",
            'parameters': {
                'id': { 
                    'is_required': True,
                    'examples': [ 'https://arax.ncats.io/api/arax/v1.0/message/1' ],
                    'default': '',
                    'type': 'string',
                    'description': """A URL/URI that identifies the Message to be fetched""",
                    },
            }
        }

        if describe:
            return command_definition


        #### Define a default response
        response = ARAXResponse()
        self.response = response
        self.message = message

        #### Basic checks on arguments
        if not isinstance(input_parameters, dict):
            response.error("Provided parameters is not a dict", error_code="ParametersNotDict")
            return response

        #### Define a complete set of allowed parameters and their defaults
        parameters = {
            'uri': None,
        }

        #### Loop through the input_parameters and override the defaults and make sure they are allowed
        for key,value in input_parameters.items():
            if key not in parameters:
                response.error(f"Supplied parameter {key} is not permitted", error_code="UnknownParameter")
            else:
                parameters[key] = value
        #### Return if any of the parameters generated an error (showing not just the first one)
        if response.status != 'OK':
            return response

        #### Store these final parameters for convenience
        response.data['parameters'] = parameters
        self.parameters = parameters


        #### Basic checks on arguments
        message_uri = input_parameters['uri']
        if not isinstance(message_uri, str):
            response.error("Provided parameter is not a string", error_code="ParameterNotString")
            return response

        response.info(f"Fetching Message via GET to '{message_uri}'")
        response_content = requests.get(message_uri, headers={'accept': 'application/json'})
        status_code = response_content.status_code

        if status_code != 200:
            response.error(f"GET to '{message_uri}' returned HTTP code {status_code} and content '{response_content.content}'", error_code="GETFailed")
            response.error(f"GET to '{message_uri}' returned HTTP code {status_code}", error_code="GETFailed")
            return response

        #### Unpack the response content into a dict and dump
        try:
            response_dict = response_content.json()
            envelope = Response().from_dict(response_dict)
        except:
            response.error(f"Error converting response from '{message_uri}' to objects from content", error_code="UnableToParseContent")
            return response

        #### Store the decoded message and return response
        message = envelope.message
        self.message = message
        self.envelope = envelope
        n_results = 0
        n_qg_nodes = 0
        n_kg_nodes = 0
        if message.results is not None and isinstance(message.results,list):
            n_results = len(message.results)
        if message.query_graph is not None and isinstance(message.query_graph,QueryGraph) and isinstance(message.query_graph.nodes,list):
            n_qg_nodes = len(message.query_graph.nodes)
        if message.knowledge_graph is not None and isinstance(message.knowledge_graph,KnowledgeGraph) and isinstance(message.knowledge_graph.nodes,list):
            n_kg_nodes = len(message.knowledge_graph.nodes)
        response.info(f"Retreived Message with {n_qg_nodes} QueryGraph nodes, {n_kg_nodes} KnowledgeGraph nodes, and {n_results} results")
        if n_qg_nodes > 0 and n_kg_nodes > 0 and n_results > 0:
            resultifier = ARAXResultify()
            response.envelope = envelope
            resultifier.recompute_qg_keys(response)
        return response


    #### Convert a Message as a dict to a Message as objects
    def from_dict(self, message):

        if str(message.__class__) == "<class 'openapi_server.models.message.Message'>":
            return message


        #### 2021-02 Temporary hack. Convert some lists into strings and some strings in lists
        #print("Fixing edges", file=sys.stderr, flush=True)
        if 'query_graph' in message and message['query_graph'] is not None:
            if 'edges' in message['query_graph'] and message['query_graph']['edges'] is not None:
                for edge_key, edge in message['query_graph']['edges'].items():
                    if 'predicates' in edge and edge['predicates'] is not None:
                        if isinstance(edge['predicates'], str):
                            edge['predicates'] = [ edge['predicates'] ]

            #print("Fixing nodes", file=sys.stderr, flush=True)
            if 'nodes' in message['query_graph'] and message['query_graph']['nodes'] is not None:
                for node_key, node in message['query_graph']['nodes'].items():
                    if 'categories' in node and node['categories'] is not None:
                        if isinstance(node['categories'], str):
                            #eprint(f"node {node_key} categories {node['categories']}")
                            node['categories'] = [ node['categories'] ]
                    if 'ids' in node and node['ids'] is not None:
                        if isinstance(node['ids'], str):
                            #eprint(f"node {node_key} ids {node['ids']}")
                            node['ids'] = [ node['ids'] ]

        if 'knowledge_graph' in message and message['knowledge_graph'] is not None:
            if 'nodes' in message['knowledge_graph'] and message['knowledge_graph']['nodes'] is not None:
                for node_key, node in message['knowledge_graph']['nodes'].items():
                    if 'categories' in node and node['categories'] is not None:
                        if isinstance(node['categories'], str):
                            node['categories'] = [ node['categories'] ]


        #eprint("Done")
        #eprint(json.dumps(message,indent=2,sort_keys=True))

        #### Deserialize
        message = Message().from_dict(message)

        #### Revert some things back temporarily

#        if message.query_graph is not None:
#            print("DEFixing edges", file=sys.stderr, flush=True)
#            if message.query_graph.edges is not None:
#                for edge_key, edge in message.query_graph.edges.items():
#                    if edge.predicate is not None:
#                        if isinstance(edge.predicate, list):
#                            if len(edge.predicate) > 0:
#                                edge.predicate = edge.predicate[0]
#                            else:
#                                edge.predicate = None

#            print("DEFixing nodes", file=sys.stderr, flush=True)
#            if message.query_graph.nodes is not None:
#                for node_key, node in message.query_graph.nodes.items():
#                    if node.category is not None:
#                        if isinstance(node.category, list):
#                            if len(node.category) > 0:
#                                node.category = node.category[0]
#                            else:
#                                node.category = None
                        # id is NOT fixed




        # When tested from ARAX_query_graph_interpreter, none of this subsequent stuff is needed

        #print(message.query_graph.__class__)
        #message.query_graph = QueryGraph().from_dict(message.query_graph)
        #message.knowledge_graph = KnowledgeGraph().from_dict(message.knowledge_graph)

        #### This is an unfortunate hack that fixes qnode.curie entries
        #### Officially a curie can be a str or a list. But Swagger 2.0 only permits one type and we set it to str
        #### so when it gets converted from_dict, the list gets converted to a str because that's its type
        #### Here we force it back. This should no longer be needed when we are properly on OpenAPI 3.0
        #if message.query_graph is not None and message.query_graph.nodes is not None:
        #    for qnode_key,qnode in message.query_graph.nodes.items():
        #        print(qnode.__class__)
                #if qnode.id is not None and isinstance(qnode.id,str):
                #    if qnode.id[0:2] == "['":
                #        try:
                #            qnode.id = ast.literal_eval(qnode.id)
                #        except:
                #            pass

        #new_nodes = []
        #for qnode in message.query_graph.nodes:
        #    print(type(qnode))
        #    new_nodes.append(QNode().from_dict(qnode))
        #message.query_graph.nodes = new_nodes
        #for qedge in message.query_graph.edges:
        #    new_edges.append(QEdge().from_dict(qedge))
        #message.query_graph.edges = new_edges

        #if message.results is not None:
        #    for result in message.results:
        #        if result.result_graph is not None:
        #            #eprint(str(result.result_graph.__class__))
        #            if str(result.result_graph.__class__) != "<class 'openapi_server.models.knowledge_graph.KnowledgeGraph'>":
        #                result.result_graph = KnowledgeGraph().from_dict(result.result_graph)

        return message



##########################################################################################
def main():

    #### Create a response object that contains the final results of our efforts
    response = ARAXResponse()
    #### Setting the output to STDERR will write out information as we go along in addition to supplying it with the response
    #ARAXResponse.output = 'STDERR'

    #### Create the ARAXMessenger
    messenger = ARAXMessenger()

    #### Test fetch_message()
    if False:
        messenger = ARAXMessenger()
        result = messenger.apply_fetch_message(messenger.message, { 'uri': 'https://arax.ncats.io/api/arax/v1.0/message/1'} )
        response.merge(result)
        if result.status != 'OK':
            print(response.show(level=ARAXResponse.DEBUG))
            return response
        message = messenger.message
        print(response.show(level=ARAXResponse.DEBUG))
        #print(json.dumps(message.to_dict(),sort_keys=True,indent=2))
        return

    #### Create an envelope in which to work
    messenger.create_envelope(response)
    if response.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response
    message = response.envelope.message

    #### Some qnode examples
    parameters_sets = [
        { 'ids': ['DOID:9281']},
        { 'ids': ['Orphanet:673']},
        { 'name': 'acetaminophen', 'categories': ['biolink:ChemicalEntity'] },
        { 'ids': ['NCIT:C198']},
        { 'ids': ['UMLS:C4710278']},
        { 'categories': ['biolink:Protein'], 'key': 'n10'},
        { 'ids': ['UniProtKB:P14136','UniProtKB:P35579'] },
        { 'ids': ['UniProtKB:P14136','UniProtKB:P35579'], 'is_set': 'false' },
        { 'ids': ['UniProtKB:P14136'], 'is_set': 'true' },
    ]

    for parameter in parameters_sets:
        #### Add a QNode
        messenger.add_qnode(response, parameter)
        if response.status != 'OK':
            print(response.show(level=ARAXResponse.DEBUG))
            return response

    #### Some qedge examples
    parameters_sets = [
        { 'subject': 'n00', 'object': 'n01' },
        { 'subject': 'n01', 'object': 'n10', 'predicates': [ 'biolink:treats' ] },
   ]

    for parameter in parameters_sets:
        #### Add a QEdge
        messenger.add_qedge(response, parameter)
        if response.status != 'OK':
            print(response.show(level=ARAXResponse.DEBUG))
            return response

    #### Delete one of the edges
    messenger.remove_qedge(response, { 'key': 'e00' } )
    if response.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response


    #### Show the final result
    print(response.show(level=ARAXResponse.DEBUG))
    print(json.dumps(response.envelope.to_dict(),sort_keys=True,indent=2))


if __name__ == "__main__": main()


# code/ARAX/ARAXQuery/ARAX_overlay.py

#!/bin/env python3
import sys


def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)


import os
import json
import ast
import re
import numpy as np
from ARAX_response import ARAXResponse
from collections import Counter
import traceback
import itertools

from ARAX_decorator import ARAXDecorator


class ARAXOverlay:

    #### Constructor
    def __init__(self):
        self.response = None
        self.message = None
        self.parameters = None
        self.allowable_actions = {
            'compute_ngd',
            'overlay_clinical_info',
            'compute_jaccard',
            'add_node_pmids',
            'predict_drug_treats_disease',
            'fisher_exact_test',
            'overlay_exposures_data'
        }
        self.report_stats = True
        self.decorator = ARAXDecorator()

        # parameter descriptions
        self.default_value_info = {
            'is_required': False,
            'examples': ['0', 'inf'],
            'default': 'inf',
            'type': 'string',
            'description': 'The default value of the normalized Google distance (if its value cannot be determined)'
        }
        self.virtual_relation_label_info = {
            'is_required': False,
            'examples': ['N1', 'N2'],
            'type': 'string',
            'description': 'Any string label identifying the virtual edge label (optional, otherwise applied to all existing edges in the KG)'
        }
        self.subject_qnode_key_info = {
            'is_required': False,
            'examples': ['n00', 'n01'],
            'type': 'string',
            'description': 'A specific subject query node id (optional, otherwise applied to all edges, must have a virtual_relation_label to use this parameter)'
        }
        self.object_qnode_key_info = {
            'is_required': False,
            'examples': ['n00', 'n01'],
            'type': 'string',
            'description': 'A specific object query node id (optional, otherwise applied to all edges, must have a virtual_relation_label to use this parameter)'
        }
        self.paired_concept_frequency_info = {
            'is_required': False,
            'examples': ['true', 'false'],
            'type': 'boolean',
            'description': "Indicates if you want to use the paired concept frequency option. Mutually exlisive with: " + \
                           "`paired_concept_frequency`, `observed_expected_ratio`, and `chi_square` if any of the oters are set to true while this is there will be an error."
        }
        self.observed_expected_ratio_info = {
            'is_required': False,
            'examples': ['true', 'false'],
            'type': 'boolean',
            'description': "Indicates if you want to use the paired concept frequency option. Mutually exlisive with: " + \
                           "`paired_concept_frequency`, `observed_expected_ratio`, and `chi_square` if any of the oters are set to true while this is there will be an error."
        }
        self.chi_square_info = {
            'is_required': False,
            'examples': ['true', 'false'],
            'type': 'boolean',
            'description': "Indicates if you want to use the paired concept frequency option. Mutually exlisive with: " + \
                           "`paired_concept_frequency`, `observed_expected_ratio`, and `chi_square` if any of the oters are set to true while this is there will be an error."
        }
        self.virtual_relation_label_info = {
            'is_required': False,
            'examples': ['N1', 'J2'],
            'type': 'string',
            'description': "An optional label to help identify the virtual edge in the relation field."
        }
        self.max_num_info = {
            'is_required': False,
            'examples': ['all', 5, 50],
            'type': 'int or string',
            'description': "The maximum number of values to return. Enter 'all' to return everything",
            'default': 100
        }
        self.start_node_key_info = {
            'is_required': True,
            'examples': ['DOID:1872', 'CHEBI:7476', 'UMLS:C1764836'],
            'type': 'string',
            'description': "A curie id specifying the starting node"
        }
        self.intermediate_node_key_info = {
            'is_required': True,
            'examples': ['DOID:1872', 'CHEBI:7476', 'UMLS:C1764836'],
            'type': 'string',
            'description': "A curie id specifying the intermediate node"
        }
        self.end_node_key_info = {
            'is_required': True,
            'examples': ['DOID:1872', 'CHEBI:7476', 'UMLS:C1764836'],
            'type': 'string',
            'description': "A curie id specifying the ending node"
        }
        self.virtual_relation_label_required_info = {
            'is_required': True,
            'examples': ['N1', 'J2', 'FET'],
            'type': 'string',
            'description': "An optional label to help identify the virtual edge in the relation field."
        }
        self.subject_qnode_key_required_info = {
            'is_required': True,
            'examples': ['n00', 'n01'],
            'type': 'string',
            'description': 'A specific subject query node id (required)'
        }
        self.object_qnode_key_required_info = {
            'is_required': True,
            'examples': ['n00', 'n01'],
            'type': 'string',
            'description': 'A specific object query node id (required)'
        }
        self.rel_edge_key_info = {
            'is_required': False,
            'examples': ['e00', 'e01'],
            'type': 'string',
            'description': "A specific QEdge id of edges connected to both subject nodes and object nodes in message KG (optional, otherwise all edges connected to both subject nodes and object nodes in message KG are considered), eg. 'e01'"
        }
        self.COHD_method_info = {
            "is_required": False,
            # "examples": ['paired_concept_frequency', 'observed_expected_ratio', 'chi_square'],
            "enum": ['paired_concept_frequency', 'observed_expected_ratio', 'chi_square'],
            "default": "paired_concept_frequency",
            "type": "string",
            "description": "Which measure from COHD should be considered."
        }
        self.filter_type_info = {
            "is_required": False,
            "examples": ['top_n', 'cutoff', None],
            "enum": ['top_n', 'cutoff', None],
            'type': 'string or None',
            'description': "If `top_n` is set this indicate the top number (the smallest) of p-values will be returned acording to what is specified in the `value` parameter. If `cutoff` is set then this indicates the p-value cutoff should be used to return results acording to what is specified in the `value` parameter. (optional, otherwise all results returned)",
            'default': None,
            'depends_on': 'value'
        }
        self.fet_value_info = {
            "is_required": False,
            "examples": ['all', 0.05, 0.95, 5, 50],
            'type': 'int or float or None',
            'description': "If `top_n` is set for `filter_type` this is an int indicating the top number (the smallest) of p-values to return. If instead `cutoff` is set then this is a float indicating the p-value cutoff to return the results. (optional, otherwise all results returned)",
            'default': None
        }
        self.dtd_threshold_info = {
            "is_required": False,
            "examples": [0.8, 0.95, 0.5],
            'type': 'int or float or None',
            'description': "What cut-off/threshold to use for DTD probability (optional, the default is 0.8)",
            'default': 0.8
        }
        self.dtd_slow_mode_info = {
            "is_required": False,
            "examples": ["True", "False"],
            "enum": ['T', 't', 'True', 'F', 'f', 'False'],
            'type': 'boolean',
            'description': "Whether to call DTD model directly rather than the precomputed DTD database to do a real-time calculation for DTD probability (default is False)",
            'default': "false"
        }

        # descriptions
        self.command_definitions = {
            "compute_ngd": {
                "dsl_command": "overlay(action=compute_ngd)",
                "description": """
`compute_ngd` computes a metric (called the normalized Google distance) based on edge soure/object node co-occurrence in abstracts of all PubMed articles.
This information is then included as an edge attribute with the name `normalized_google_distance`.
You have the choice of applying this to all edges in the knowledge graph, or only between specified subject/object qnode id's. If the later, virtual edges are added with the type specified by `virtual_relation_label`.

Use cases include:

* focusing in on edges that are well represented in the literature
* focusing in on edges that are under-represented in the literature

This can be applied to an arbitrary knowledge graph as possible edge types are computed dynamically (i.e. not just those created/recognized by the ARA Expander team).
                    """,
                'brief_description': """
compute_ngd computes a metric (called the normalized Google distance) based on edge soure/object node co-occurrence in abstracts of all PubMed articles.
This information is then included as an edge attribute with the name normalized_google_distance.
You have the choice of applying this to all edges in the knowledge graph, or only between specified subject/object qnode id's. If the later, virtual edges are added with the type specified by virtual_relation_label.
                    """,
                "parameters": {
                    'default_value': self.default_value_info,
                    'virtual_relation_label': self.virtual_relation_label_info,
                    'subject_qnode_key': self.subject_qnode_key_info,
                    'object_qnode_key': self.object_qnode_key_info
                }
            },
            "overlay_clinical_info": {
                "dsl_command": "overlay(action=overlay_clinical_info)",
                "description": """
`overlay_clinical_info` overlay edges with information obtained from the knowledge provider (KP) Columbia Open Health Data (COHD).
This KP has a number of different functionalities, such as `paired_concept_frequency`, `observed_expected_ratio`, etc. which are mutually exclusive DSL parameters.
All information is derived from a 5 year hierarchical dataset: Counts for each concept include patients from descendant concepts. 
This includes clinical data from 2013-2017 and includes 1,731,858 different patients.
This information is then included as an edge attribute.
You have the choice of applying this to all edges in the knowledge graph, or only between specified subject/object qnode id's. If the later, virtual edges are added with the relation specified by `virtual_relation_label`.
These virtual edges have the following types:

* `paired_concept_frequency` has the virtual edge type `has_paired_concept_frequency_with`
* `observed_expected_ratio` has the virtual edge type `has_observed_expected_ratio_with`
* `chi_square` has the virtual edge type `has_chi_square_with`

Note that this DSL command has quite a bit of functionality, so a brief description of the DSL parameters is given here:

* `paired_concept_frequency`: If set to `true`, retrieves observed clinical frequencies of a pair of concepts indicated by edge subject and object nodes and adds these values as edge attributes.
* `observed_expected_ratio`: If set to `true`, returns the natural logarithm of the ratio between the observed count and expected count of edge subject and object nodes. Expected count is calculated from the single concept frequencies and assuming independence between the concepts. This information is added as an edge attribute.
* `chi_square`: If set to `true`, returns the chi-square statistic and p-value between pairs of concepts indicated by edge subject/object nodes and adds these values as edge attributes. The expected frequencies for the chi-square analysis are calculated based on the single concept frequencies and assuming independence between concepts. P-value is calculated with 1 DOF.
* `virtual_edge_type`: Overlays the requested information on virtual edges (ones that don't exist in the query graph).

This can be applied to an arbitrary knowledge graph as possible edge types are computed dynamically (i.e. not just those created/recognized by the ARA Expander team).
                    """,
                'brief_description': """
overlay_clinical_info overlay edges with information obtained from the knowledge provider (KP) Columbia Open Health Data (COHD).
This KP has a number of different functionalities, such as 'paired_concept_frequency', 'observed_expected_ratio', etc. which are mutually exclusive DSL parameters.
All information is derived from a 5 year hierarchical dataset: Counts for each concept include patients from descendant concepts. 
This includes clinical data from 2013-2017 and includes 1,731,858 different patients.
This information is then included as an edge attribute.
                    """,
                "mutually_exclusive_params": [
                    'paired_concept_frequency',
                    'observed_expected_ratio',
                    'chi_square'
                ],
                "parameters": {
                    'COHD_method': self.COHD_method_info,
                    'virtual_relation_label': self.virtual_relation_label_info,
                    'subject_qnode_key': self.subject_qnode_key_info,
                    'object_qnode_key': self.object_qnode_key_info
                }
            },
            "compute_jaccard": {
                "dsl_command": "overlay(action=compute_jaccard)",
                "description": """
`compute_jaccard` creates virtual edges and adds an edge attribute (with the property name `jaccard_index`) containing the following information:
The jaccard similarity measures how many `intermediate_node_key`'s are shared in common between each `start_node_key` and `object_node_key`.
This is used for purposes such as "find me all drugs (`start_node_key`) that have many proteins (`intermediate_node_key`) in common with this disease (`end_node_key`)."
This can be used for downstream filtering to concentrate on relevant bioentities.

This can be applied to an arbitrary knowledge graph as possible edge types are computed dynamically (i.e. not just those created/recognized by the ARA Expander team).
                    """,
                'brief_description': """
compute_jaccard creates virtual edges and adds an edge attribute (with the property name 'jaccard_index') containing the following information:
The jaccard similarity measures how many 'intermediate_node_key's are shared in common between each 'start_node_key' and 'object_node_key'.
This is used for purposes such as "find me all drugs ('start_node_key') that have many proteins ('intermediate_node_key') in common with this disease ('end_node_key')."
This can be used for downstream filtering to concentrate on relevant bioentities.
                    """,
                "parameters": {
                    'start_node_key': self.start_node_key_info,
                    'intermediate_node_key': self.intermediate_node_key_info,
                    'end_node_key': self.end_node_key_info,
                    'virtual_relation_label': self.virtual_relation_label_required_info
                }
            },
            "add_node_pmids": {
                "dsl_command": "overlay(action=add_node_pmids)",
                "description": """
`add_node_pmids` adds PubMed PMID's as node attributes to each node in the knowledge graph.
This information is obtained from mapping node identifiers to MeSH terms and obtaining which PubMed articles have this MeSH term
either labeling in the metadata or has the MeSH term occurring in the abstract of the article.

This can be applied to an arbitrary knowledge graph as possible edge types are computed dynamically (i.e. not just those created/recognized by the ARA Expander team).
                    """,
                'brief_description': """
add_node_pmids adds PubMed PMID's as node attributes to each node in the knowledge graph.
This information is obtained from mapping node identifiers to MeSH terms and obtaining which PubMed articles have this MeSH term
either labeling in the metadata or has the MeSH term occurring in the abstract of the article.
                    """,
                "parameters": {
                    'max_num': self.max_num_info
                }
            },
            "predict_drug_treats_disease": {
                "dsl_command": "overlay(action=predict_drug_treats_disease)",
                "description": """
`predict_drug_treats_disease` utilizes a machine learning model (trained on KP ARAX/KG1) to assign a probability that a given drug/chemical_substance treats a disease/phenotypic feature.
For more information about how this model was trained and how it performs, please see [this publication](https://doi.org/10.1101/765305).
The drug-disease treatment prediction probability is included as an edge attribute (with the attribute name `probability_treats`).
You have the choice of applying this to all appropriate edges in the knowledge graph, or only between specified subject/object qnode id's (make sure one is a chemical_substance, and the other is a disease or phenotypic_feature). 
If the later, virtual edges are added with the relation specified by `virtual_edge_type` and the type `probably_treats`.
Use cases include:

* Overlay drug the probability of any drug in your knowledge graph treating any disease via `overlay(action=predict_drug_treats_disease)`
* For specific drugs and diseases/phenotypes in your graph, add the probability that the drug treats them with something like `overlay(action=predict_drug_treats_disease, subject_qnode_key=n02, object_qnode_key=n00, virtual_relation_label=P1)`
* Subsequently remove low-probability treating drugs with `overlay(action=predict_drug_treats_disease)` followed by `filter_kg(action=remove_edges_by_attribute, edge_attribute=probability_treats, direction=below, threshold=.6, remove_connected_nodes=t, qnode_key=n02)`

This can be applied to an arbitrary knowledge graph as possible edge types are computed dynamically (i.e. not just those created/recognized by the ARA Expander team).
                    """,
                'brief_description': """
predict_drug_treats_disease utilizes a machine learning model (trained on KP RTX-KG2C) to assign a probability that a given drug/chemical_substance treats a disease/phenotypic feature.
For more information about how this model was trained and how it performs, please see this publication (https://doi.org/10.1101/765305) for the version trained on KG1 which used node2vec for its
embeddings. The current version uses KG2C; publication in preparation.
The drug-disease treatment prediction probability is included as an edge attribute (with the attribute name 'probability_treats').
You have the choice of applying this to all appropriate edges in the knowledge graph, or only between specified subject/object qnode id's (make sure one is a chemical_substance, and the other is a disease or phenotypic_feature). 
                    """,
                "parameters": {
                    'virtual_relation_label': self.virtual_relation_label_info,
                    'subject_qnode_key': self.subject_qnode_key_info,
                    'object_qnode_key': self.object_qnode_key_info,
                    'threshold': self.dtd_threshold_info,
                    'slow_mode': self.dtd_slow_mode_info
                }
            },
            "fisher_exact_test": {
                "dsl_command": "overlay(action=fisher_exact_test)",
                "description": """
`fisher_exact_test` computes the Fisher's Exact Test p-values of the connection between a list of given nodes with specified query id (subject_qnode_key eg. 'n01') to their adjacent nodes with specified query id (e.g. object_qnode_key 'n02') in the message knowledge graph. 
This information is then added as an edge attribute to a virtual edge which is then added to the query graph and knowledge graph.
It can also allow you to filter out the user-defined insignificance of connections based on a specified p-value cutoff or return the top n smallest p-value of connections and only add their corresponding virtual edges to the knowledge graph.

This can be applied to an arbitrary knowledge graph as possible edge types are computed dynamically (i.e. not just those created/recognized by the ARA Expander team).

Use cases include:

* Given an input list (or a single) bioentities with specified query id in message KG, find connected bioentities  that are most "representative" of the input list of bioentities
* Find biological pathways that are enriched for an input list of proteins (specified with a query id)
* Make long query graph expansions in a targeted fashion to reduce the combinatorial explosion experienced with long query graphs 

This p-value is calculated from fisher's exact test based on the contingency table with following format:

|||||
|-----|-----|-----|-----|
|                                  | in query node list | not in query node list | row total |
| connect to certain adjacent node |         a          |           b            |   a+b     |
| not connect to adjacent node     |         c          |           d            |   c+d     |
|         column total             |        a+c         |          b+d           |  a+b+c+d  |

The p-value is calculated by applying fisher_exact method of scipy.stats module in scipy package to the contingency table.
The code is as follows:

```
_, pvalue = stats.fisher_exact([[a, b], [c, d]])
```
                    """,
                'brief_description': """
fisher_exact_test computes the Fisher's Exact Test p-values of the connection between a list of given nodes with specified query id (subject_qnode_key eg. 'n01') to their adjacent nodes with specified query id (e.g. object_qnode_key 'n02') in the message knowledge graph. 
This information is then added as an edge attribute to a virtual edge which is then added to the query graph and knowledge graph.
It can also allow you to filter out the user-defined insignificance of connections based on a specified p-value cutoff or return the top n smallest p-value of connections and only add their corresponding virtual edges to the knowledge graph.
                    """,
                "parameters": {
                    'subject_qnode_key': self.subject_qnode_key_required_info,
                    'virtual_relation_label': self.virtual_relation_label_required_info,
                    'object_qnode_key': self.object_qnode_key_required_info,
                    'rel_edge_key': self.rel_edge_key_info,
                    'filter_type': self.filter_type_info,
                    'value': self.fet_value_info
                }
            },
            "overlay_exposures_data": {
                "dsl_command": "overlay(action=overlay_exposures_data)",
                "description": """
`overlay_exposures_data` overlays edges with p-values obtained from the ICEES+ (Integrated Clinical and Environmental Exposures Service) knowledge provider.
This information is included in edge attributes with the name `icees_p-value`.
You have the choice of applying this to all edges in the knowledge graph, or only between specified subject/object qnode IDs. If the latter, the data is added in 'virtual' edges with the type `has_icees_p-value_with`.

This can be applied to an arbitrary knowledge graph (i.e. not just those created/recognized by Expander Agent).
                    """,
                'brief_description': """
overlay_exposures_data overlays edges with p-values obtained from the ICEES+ (Integrated Clinical and Environmental Exposures Service) knowledge provider.
This information is included in edge attributes with the name 'icees_p-value'.
                    """,
                "parameters": {
                    'virtual_relation_label': self.virtual_relation_label_info,
                    'subject_qnode_key': self.subject_qnode_key_info,
                    'object_qnode_key': self.object_qnode_key_info
                }
            }
        }

    def report_response_stats(self, response):
        """
        Little helper function that will report the KG, QG, and results stats to the debug in the process of executing actions. Basically to help diagnose problems
        """
        message = self.message
        if self.report_stats:
            # report number of nodes and edges, and their type in the QG
            if hasattr(message, 'query_graph') and message.query_graph:
                response.debug(f"Query graph is {message.query_graph}")
            if hasattr(message, 'knowledge_graph') and message.knowledge_graph and hasattr(message.knowledge_graph,
                                                                                           'nodes') and message.knowledge_graph.nodes and hasattr(
                    message.knowledge_graph, 'edges') and message.knowledge_graph.edges:
                response.debug(f"Number of nodes in KG is {len(message.knowledge_graph.nodes)}")
                # This works for KG1 and KG2
                response.debug(
                    f"Number of nodes in KG by type is {Counter([x.categories[0] for x in message.knowledge_graph.nodes.values() if x.categories])}")  # type is a list, just get the first one
                    #f"Number of nodes in KG by type is {Counter([x for x in message.knowledge_graph.nodes.values()])}")
                # don't really need to worry about this now
                # response.debug(f"Number of nodes in KG by with attributes are {Counter([x.category for x in message.knowledge_graph.nodes.values()])}")
                response.debug(f"Number of edges in KG is {len(message.knowledge_graph.edges)}")
                response.debug(
                    f"Number of edges in KG by type is {Counter([x.predicate for x in message.knowledge_graph.edges.values()])}")
                response.debug(
                    f"Number of edges in KG with attributes is {len([x for x in message.knowledge_graph.edges.values() if x.attributes])}")
                # Collect attribute names, could do this with list comprehension, but this is so much more readable
                attribute_names = []
                for x in message.knowledge_graph.edges.values():
                    if x.attributes:
                        for attr in x.attributes:
                            attribute_names.append(attr.original_attribute_name)
                response.debug(f"Number of edges in KG by attribute {Counter(attribute_names)}")
        return response

    def describe_me(self):
        """
        Little helper function for internal use that describes the actions and what they can do
        :return:
        """
        description_list = []
        for action in self.allowable_actions:
            description_list.append(getattr(self, '_' + self.__class__.__name__ + '__' + action)(describe=True))
        return description_list

    def check_params(self, allowable_parameters):
        # Write a little helper function to test parameters
        """
        Checks to see if the input parameters are allowed
        :param input_parameters: input parameters supplied to ARAXOverlay.apply()
        :param allowable_parameters: the allowable parameters
        :return: None
        """
        # allowable_parameters = self.command_definitions['parameters']
        for key, item in self.parameters.items():
            if key not in allowable_parameters:
                self.response.error(
                    f"Supplied parameter {key} is not permitted. Allowable parameters are: {list(allowable_parameters.keys())}",
                    error_code="UnknownParameter")
            elif item not in allowable_parameters[key]:
                if any([type(x) == float for x in allowable_parameters[key]]) or any([type(x) == int for x in
                                                                                      allowable_parameters[
                                                                                          key]]):  # if it's a float or int, just accept it as it is
                    return
                elif key == "virtual_relation_label" and type(item) == str:
                    return
                else:  # otherwise, it's really not an allowable parameter
                    self.response.error(
                        f"Supplied value {item} is not permitted. In action {allowable_parameters['action']}, allowable values to {key} are: {list(allowable_parameters[key])}",
                        error_code="UnknownValue")

    # helper function to check if all virtual edge parameters have been properly provided
    def check_virtual_edge_params(self, allowable_parameters):
        parameters = self.parameters
        if any([x in ['virtual_relation_label', 'subject_qnode_key', 'object_qnode_key'] for x in parameters.keys()]):
            if not all([x in parameters.keys() for x in
                        ['virtual_relation_label', 'subject_qnode_key', 'object_qnode_key']]):
                self.response.error(
                    f"If any of of the following parameters are provided ['virtual_relation_label', 'subject_qnode_key', 'object_qnode_key'], all must be provided. Allowable parameters include: {allowable_parameters}")
            elif parameters['subject_qnode_key'] not in allowable_parameters['subject_qnode_key']:
                self.response.error(
                    f"subject_qnode_key value is not valid. Valid values are: {allowable_parameters['subject_qnode_key']}")
            elif parameters['object_qnode_key'] not in allowable_parameters['object_qnode_key']:
                self.response.error(
                    f"object_qnode_key value is not valid. Valid values are: {allowable_parameters['object_qnode_key']}")

    #### Top level decision maker for applying filters
    def apply(self, response, input_parameters):

        #### Define a default response
        if response is None:
            response = ARAXResponse()
        self.response = response
        self.message = response.envelope.message

        #### Basic checks on arguments
        if not isinstance(input_parameters, dict):
            response.error("Provided parameters is not a dict", error_code="ParametersNotDict")
            return response

        # list of actions that have so far been created for ARAX_overlay
        allowable_actions = self.allowable_actions

        # check to see if an action is actually provided
        if 'action' not in input_parameters:
            response.error(f"Must supply an action. Allowable actions are: action={allowable_actions}",
                           error_code="MissingAction")
        elif input_parameters['action'] not in allowable_actions:
            response.error(
                f"Supplied action {input_parameters['action']} is not permitted. Allowable actions are: {allowable_actions}",
                error_code="UnknownAction")

        #### Return if any of the parameters generated an error (showing not just the first one)
        if response.status != 'OK':
            return response

        # populate the parameters dict
        parameters = dict()
        for key, value in input_parameters.items():
            parameters[key] = value

        #### Store these final parameters for convenience
        response.data['parameters'] = parameters
        self.parameters = parameters

        response.debug(
            f"Applying Overlay to Message with parameters {parameters}")  # TODO: re-write this to be more specific about the actual action

        # Don't try to overlay anything if the KG is empty
        message = response.envelope.message
        if not message.knowledge_graph or not message.knowledge_graph.nodes:
            response.debug(f"Nothing to overlay (KG is empty)")
            return response
        # Don't try to overlay anything if any of the specified qnodes aren't fulfilled in the KG
        possible_node_params = {"subject_qnode_key", "object_qnode_key", "start_node_key", "intermediate_node_key",
                                "end_node_key"}
        node_params_to_check = set(self.parameters).intersection(possible_node_params)
        qnode_keys_to_check = {self.parameters[node_param] for node_param in node_params_to_check}
        if not all(any(node for node in message.knowledge_graph.nodes.values() if qnode_key in node.qnode_keys)
                   for qnode_key in qnode_keys_to_check):
            response.debug(f"Nothing to overlay (one or more of the specified qnodes is not fulfilled in the KG)")
            return response

        # convert the action string to a function call (so I don't need a ton of if statements
        getattr(self, '_' + self.__class__.__name__ + '__' + parameters[
            'action'])()  # thank you https://stackoverflow.com/questions/11649848/call-methods-by-string

        # TODO: add_pubmed_ids
        # TODO: compute_confidence_scores
        # TODO: finish COHD
        # TODO: Jaccard

        #### Return the response and done
        if self.report_stats:  # helper to report information in debug if class self.report_stats = True
            response = self.report_response_stats(response)
        return response

    def __compute_ngd(self, describe=False):
        """
        Computes normalized google distance between two nodes connected by an edge in the knowledge graph
        and adds that as an edge attribute.
        Allowable parameters: {default_value: {'0', 'inf'}}
        :return:
        """
        message = self.message
        parameters = self.parameters
        # make a list of the allowable parameters (keys), and their possible values (values). Note that the action and corresponding name will always be in the allowable parameters
        # allowable_parameters = {'action': {'compute_ngd'}, 'default_value': {'0', 'inf'}}
        if message and parameters and hasattr(message, 'query_graph') and hasattr(message.query_graph, 'edges'):
            allowable_parameters = {'action': {'compute_ngd'}, 'default_value': {'0', 'inf'},
                                    'virtual_relation_label': {self.parameters[
                                                                   'virtual_relation_label'] if 'virtual_relation_label' in self.parameters else None},
                                    'subject_qnode_key': set([key for key in self.message.query_graph.nodes.keys()]),
                                    'object_qnode_key': set([key for key in self.message.query_graph.nodes.keys()])
                                    }
        else:
            allowable_parameters = {'action': {'compute_ngd'}, 'default_value': {'0', 'inf'},
                                    'virtual_relation_label': {
                                        'any string label identifying the virtual edge label (optional, otherwise applied to all existing edges in the KG)'},
                                    'subject_qnode_key': {
                                        'a specific subject query node id (optional, otherwise applied to all edges)'},
                                    'object_qnode_key': {
                                        'a specific object query node id (optional, otherwise applied to all edges)'}
                                    }

        # A little function to describe what this thing does
        if describe:
            description_dict = self.command_definitions['compute_ngd']
            return description_dict

        # Make sure only allowable parameters and values have been passed
        # FIXME : this will need to be fixed
        self.check_params(allowable_parameters)
        # return if bad parameters have been passed
        if self.response.status != 'OK':
            return self.response

        # set the default value if it's not already done
        if 'default_value' not in parameters:
            parameters['default_value'] = np.inf
        else:
            if parameters['default_value'] == '0':
                parameters['default_value'] = '0'
            else:
                parameters['default_value'] = float("inf")

        # Check if all virtual edge params have been provided properly

        # FIXME : this will need to be fixed
        # self.check_virtual_edge_params(allowable_parameters)
        # FW: changing this to only check if subject_qnode_key or object_qnode_key is present
        if 'subject_qnode_key' in parameters or 'object_qnode_key' in parameters:
            self.check_virtual_edge_params(allowable_parameters)
        if self.response.status != 'OK':
            return self.response

        # now do the call out to NGD
        from Overlay.compute_ngd import ComputeNGD
        NGD = ComputeNGD(self.response, self.message, parameters)
        response = NGD.compute_ngd()
        self.decorator.decorate_edges(response, kind="NGD")
        return response

    def __overlay_clinical_info(self, describe=False):  # TODO: put the default paramas and all that other goodness in
        """
        This function will apply the action overlay_clinical_info.
        Allowable parameters are:
        :return: a response
        """
        message = self.message
        parameters = self.parameters

        # make a list of the allowable parameters (keys), and their possible values (values). Note that the action and corresponding name will always be in the allowable parameters
        if message and parameters and hasattr(message, 'query_graph') and hasattr(message.query_graph, 'edges'):
            allowable_parameters = {'action': {'overlay_clinical_info'},
                                    'COHD_method': {'paired_concept_frequency', 'observed_expected_ratio',
                                                    'chi_square'},
                                    'paired_concept_frequency': {'true', 'false'},
                                    'observed_expected_ratio': {'true', 'false'},
                                    'chi_square': {'true', 'false'},
                                    'virtual_relation_label': {self.parameters[
                                                                   'virtual_relation_label'] if 'virtual_relation_label' in self.parameters else None},
                                    'subject_qnode_key': set([key for key in self.message.query_graph.nodes.keys()]),
                                    'object_qnode_key': set([key for key in self.message.query_graph.nodes.keys()])
                                    }
        else:
            allowable_parameters = {'action': {'overlay_clinical_info'},
                                    'COHD_method': {'paired_concept_frequency', 'observed_expected_ratio',
                                                    'chi_square'},
                                    'paired_concept_frequency': {'true', 'false'},
                                    'observed_expected_ratio': {'true', 'false'},
                                    'chi_square': {'true', 'false'},
                                    'virtual_relation_label': {
                                        'any string label used to identify the virtual edge (optional, otherwise information is added as an attribute to all existing edges in the KG)'},
                                    'subject_qnode_key': {
                                        'a specific subject query node id (optional, otherwise applied to all edges)'},
                                    'object_qnode_key': {
                                        'a specific object query node id (optional, otherwise applied to all edges)'}
                                    }

        # A little function to describe what this thing does
        if describe:
            description_dict = self.command_definitions['overlay_clinical_info']
            return description_dict

        # Make sure only allowable parameters and values have been passed
        self.check_params(allowable_parameters)
        # return if bad parameters have been passed
        if self.response.status != 'OK':
            return self.response

        # check if conflicting parameters have been provided
        if 'COHD_method' in self.parameters:
            for method in {'paired_concept_frequency', 'observed_expected_ratio', 'chi_square'}:
                self.parameters[method] = 'false'
            self.parameters[parameters['COHD_method']] = 'true'
        elif 'paired_concept_frequency' not in self.parameters and 'observed_expected_ratio' not in self.parameters and 'chi_square' not in self.parameters:
            self.parameters['paired_concept_frequency'] = 'true'
            self.parameters['COHD_method'] = 'paired_concept_frequency'
        else:
            mutually_exclusive_params = {'paired_concept_frequency', 'observed_expected_ratio', 'chi_square'}
            if np.sum([x in mutually_exclusive_params for x in parameters]) > 1:
                self.response.error(
                    f"The parameters {mutually_exclusive_params} are mutually exclusive. Please provide only one for each call to overlay(action=overlay_clinical_info)")
        if self.response.status != 'OK':
            return self.response

        # Check if all virtual edge params have been provided properly
        # self.check_virtual_edge_params(allowable_parameters)
        # FW: changing this to only check if subject_qnode_key or object_qnode_key is present
        if 'subject_qnode_key' in parameters or 'object_qnode_key' in parameters:
            self.check_virtual_edge_params(allowable_parameters)
        if self.response.status != 'OK':
            return self.response

        # TODO: make sure that not more than one other kind of action has been asked for since COHD has a lot of functionality #606
        # TODO: make sure conflicting defaults aren't called either, partially completed
        # TODO: until then, just pass the parameters as is

        default_params = parameters  # here is where you can set default values

        from Overlay.overlay_clinical_info import OverlayClinicalInfo
        OCI = OverlayClinicalInfo(self.response, self.message, default_params)
        if OCI.response.status != 'OK':
            return OCI.response
        response = OCI.decorate()  # TODO: refactor this so it's basically another apply() like function # 606
        return response

    def __add_node_pmids(self, describe=False):
        """
        Computes normalized google distance between two nodes connected by an edge in the knowledge graph
        and adds that as an edge attribute.
        Allowable parameters: {max_num: {'all', 'any integer'}}
        :return:
        """
        message = self.message
        parameters = self.parameters
        # make a list of the allowable parameters (keys), and their possible values (values). Note that the action and corresponding name will always be in the allowable parameters
        # allowable_parameters = {'action': {'add_node_pmids'}, 'max_num': {'all', int()}}

        if message and parameters and hasattr(message, 'query_graph') and hasattr(message.query_graph, 'nodes'):
            allowable_parameters = {'action': {'add_node_pmids'}, 'max_num': {'all', int()}}
        else:
            allowable_parameters = {'action': {'add_node_pmids'}, 'max_num': {'all', 'any integer'}}

        # A little function to describe what this thing does
        if describe:
            description_dict = self.command_definitions['add_node_pmids']
            return description_dict

        # Make sure only allowable parameters and values have been passed
        self.check_params(allowable_parameters)
        # return if bad parameters have been passed
        if self.response.status != 'OK':
            return self.response

        # Set the default parameters
        pass_params = {'max_num': 100}  # here is where you can set default values

        # parse the input parameters to be the data types I need them to be
        for key, value in self.parameters.items():
            if key == 'max_num':
                if value == 'all':
                    pass_params[key] = None
                else:
                    try:
                        pass_params[key] = int(value)
                    except:
                        tb = traceback.format_exc()
                        error_type, error, _ = sys.exc_info()
                        self.response.error(tb, error_code=error_type.__name__)
                        self.response.error(f"parameter 'max_num' must be an integer")
        if self.response.status != 'OK':
            return self.response

        # now do the call out to NGD
        from Overlay.add_node_pmids import AddNodePMIDS
        ANP = AddNodePMIDS(self.response, self.message, pass_params)
        response = ANP.add_node_pmids()
        return response

    def __compute_jaccard(self, describe=False):
        """
        Computes the jaccard distance: starting_node -> {set of intermediate nodes} -> {set of end nodes}.
        for each end node x, looks at (number of intermediate nodes connected to x) / (total number of intermediate nodes).
        Basically, which of the end nodes is connected to many of the intermediate nodes. Adds an edge to the KG with the
        jaccard value, subject, and object info as an edge attribute .
        Allowable parameters:
        :return:
        """
        message = self.message
        parameters = self.parameters
        # need two different ones of these since the allowable parameters will depend on the id's that they used
        # TODO: the start_node_key CANNOT be a set
        if message and parameters and hasattr(message, 'query_graph') and hasattr(message.query_graph, 'nodes'):
            allowable_parameters = {'action': {'compute_jaccard'},
                                    'start_node_key': set([key for key in self.message.query_graph.nodes.keys()]),
                                    'intermediate_node_key': set(
                                        [key for key in self.message.query_graph.nodes.keys()]),
                                    'end_node_key': set([key for key in self.message.query_graph.nodes.keys()]),
                                    'virtual_relation_label': {self.parameters[
                                                                   'virtual_relation_label'] if 'virtual_relation_label' in self.parameters else "any_string"}
                                    }
        else:
            allowable_parameters = {'action': {'compute_jaccard'},
                                    'start_node_key': {"a node id (required)"},
                                    'intermediate_node_key': {"a query node id (required)"},
                                    'end_node_key': {"a query node id (required)"},
                                    'virtual_relation_label': {
                                        "any string label (required) that will be used to identify the virtual edge added"}
                                    }
        # print(allowable_parameters)
        # A little function to describe what this thing does
        if describe:
            description_dict = self.command_definitions['compute_jaccard']
            return description_dict

        # Make sure only allowable parameters and values have been passed
        self.check_params(allowable_parameters)
        # return if bad parameters have been passed
        if self.response.status != 'OK':
            return self.response

        # No default parameters to set

        # in the above allowable_parameters, we've already checked if the node id's exist, so no need to check them

        # now do the call out to NGD
        from Overlay.compute_jaccard import ComputeJaccard
        JAC = ComputeJaccard(self.response, self.message, self.parameters)
        response = JAC.compute_jaccard()
        return response

    def __predict_drug_treats_disease(self, describe=False):
        """
        Utilizes a machine learning model to predict if a given chemical_substance treats a disease or phenotypic_feature
        Allowable parameters:
        :return:
        """
        message = self.message
        parameters = self.parameters
        # make a list of the allowable parameters (keys), and their possible values (values). Note that the action and corresponding name will always be in the allowable parameters
        if message and parameters and hasattr(message, 'query_graph') and hasattr(message.query_graph, 'edges'):
            qg_nodes = message.query_graph.nodes
            allowable_parameters = {'action': {'predict_drug_treats_disease'}, 'virtual_relation_label': {
                self.parameters['virtual_relation_label'] if 'virtual_relation_label' in self.parameters else None},
                                    # 'subject_qnode_key': set([k for k, x in self.message.query_graph.nodes.items() if x.category == "chemical_substance"]),
                                    'subject_qnode_key': set([node_key for node_key in qg_nodes.keys()]),
                                    # allow any query node type, will be handled by predict_drug_treats_disease.py
                                    # 'object_qnode_key': set([k for k, x in self.message.query_graph.nodes.items() if (x.category == "disease" or x.category == "phenotypic_feature")])
                                    'object_qnode_key': set([node_key for node_key in qg_nodes.keys()]),
                                    'threshold': [None, int(), float()],
                                    # allow any query node type, will be handled by predict_drug_treats_disease.py
                                    'slow_mode': ["true", "false", "True", "False", "t", "f", "T", "F"]
                                    }
        else:
            allowable_parameters = {'action': {'predict_drug_treats_disease'}, 'virtual_relation_label': {
                'optional: any string label that identifies the virtual edges added (otherwise applied to all drug->disease and drug->phenotypic_feature edges)'},
                                    'subject_qnode_key': {
                                        'optional: a specific subject query node id corresponding to a disease query node (otherwise applied to all drug->disease and drug->phenotypic_feature edges)'},
                                    'object_qnode_key': {
                                        'optional: a specific object query node id corresponding to a disease or phenotypic_feature query node (otherwise applied to all drug->disease and drug->phenotypic_feature edges)'},
                                    'threshold': {
                                        'optional: What cut-off/threshold to use for DTD probability (default is 0.8)'},
                                    'slow_mode': {
                                        'optional: Whether to call DTD model rather than DTD database to do a real-time calculation for DTD probability (default is False)'}
                                    }

        # A little function to describe what this thing does
        if describe:
            description_dict = self.command_definitions['predict_drug_treats_disease']
            return description_dict

        # Make sure only allowable parameters and values have been passed
        self.check_params(allowable_parameters)
        # return if bad parameters have been passed
        if self.response.status != 'OK':
            return self.response
        # Check if all virtual edge params have been provided properly
        # self.check_virtual_edge_params(allowable_parameters)
        # FW: changing this to only check if subject_qnode_key or object_qnode_key is present
        if 'subject_qnode_key' in parameters or 'object_qnode_key' in parameters:
            self.check_virtual_edge_params(allowable_parameters)
        if self.response.status != 'OK':
            return self.response

        # now do the call out to NGD
        from Overlay.predict_drug_treats_disease import PredictDrugTreatsDisease
        PDTD = PredictDrugTreatsDisease(self.response, self.message, parameters)
        if PDTD.response.status != 'OK':
            return PDTD.response
        response = PDTD.predict_drug_treats_disease()
        return response

    def __fisher_exact_test(self, describe=False):

        """
        Computes the the Fisher's Exact Test p-value of the connection between a list of given nodes with specified query key (subject_qnode_key eg. 'n01') to their adjacent nodes with specified query key (e.g. object_qnode_key 'n02') in message knowledge graph.
        Allowable parameters:
            :param subject_qnode_key: (required) a specific QNode key (you used in add_qnode() in DSL) of subject nodes in message KG eg. "n00"
            :param virtual_relation_label: (required) any string to label the relation and query edge id of virtual edge with fisher's exact test p-value eg. 'FET'
            :param object_qnode_key: (required) a specific QNode key (you used in add_qnode() in DSL) of object nodes in message KG. This will specify which node in KG to consider for calculating the Fisher Exact Test, eg. "n01"
            :param rel_edge_key: (optional) a specific QEdge key (you used in add_qedge() in DSL) of edges connected to both subject nodes and object nodes in message KG. eg. "e01"
            :param top_n: (optional) an int indicating the top number of the most significant adjacent nodes to return (otherwise all results returned) eg. 10
            :param cutoff: (optional) a float indicating the p-value cutoff to return the results (otherwise all results returned) eg. 0.05
        :return: response
        """

        message = self.message
        parameters = self.parameters
        # make a list of the allowable parameters (keys), and their possible values (values). Note that the action and corresponding name will always be in the allowable parameters
        # allowable_parameters = {'action': {'fisher_exact_test'}, 'query_node_label': {...}, 'compare_node_label':{...}}

        if message and parameters and hasattr(message, 'query_graph') and hasattr(message.query_graph,
                                                                                  'nodes') and hasattr(
                message.query_graph, 'edges'):
            qg_nodes = message.query_graph.nodes
            qg_edges = message.query_graph.edges
            allowable_subject_qnode_key = list(
                set([node_key for node_key in qg_nodes.keys()]))  # flatten these as they are lists of lists now
            allowable_object_qnode_key = list(
                set([node_key for node_key in qg_nodes.keys()]))  # flatten these as they are lists of lists now
            allowwable_rel_edge_key = list(
                set([edge_key for edge_key in qg_edges.keys()]))  # flatten these as they are lists of lists now
            allowwable_rel_edge_key.append(None)
            # # FIXME: need to generate this from some subject as per #780
            # allowable_object_node_type = [None,'metabolite','biological_process','chemical_substance','microRNA','protein',
            #                      'anatomical_entity','pathway','cellular_component','phenotypic_feature','disease','molecular_function']
            # allowable_object_edge_type = [None,'physically_interacts_with','subclass_of','involved_in','affects','capable_of',
            #                      'contraindicated_for','indicated_for','regulates','expressed_in','gene_associated_with_condition',
            #                      'has_phenotype','gene_mutations_contribute_to','participates_in','has_part']

            allowable_parameters = {'action': {'fisher_exact_test'},
                                    'subject_qnode_key': allowable_subject_qnode_key,
                                    'virtual_relation_label': str(),
                                    'object_qnode_key': allowable_object_qnode_key,
                                    'rel_edge_key': allowwable_rel_edge_key,
                                    'top_n': [None, int()],
                                    'cutoff': [None, float()],
                                    'filter_type': {'cutoff', 'top_n'},
                                    'value': [None, int(), float()],
                                    }
        else:
            allowable_parameters = {'action': {'fisher_exact_test'},
                                    'subject_qnode_key': {
                                        "a specific QNode key of subject nodes in message KG (required), eg. 'n00'"},
                                    'virtual_relation_label': {
                                        "any string to label the relation and query edge id of virtual edge with fisher's exact test p-value (required) eg. 'FET'"},
                                    'object_qnode_key': {
                                        "a specific QNode key of object nodes in message KG. This will specify which node in KG to consider for calculating the Fisher Exact Test (required), eg. 'n01'"},
                                    'rel_edge_key': {
                                        "a specific QEdge key of edges connected to both subject nodes and object nodes in message KG (optional, otherwise all edges connected to both subject nodes and object nodes in message KG are considered), eg. 'e01'"},
                                    'top_n': {
                                        "an int indicating the top number (the smallest) of p-values to return (optional,otherwise all results returned), eg. 10"},
                                    'cutoff': {
                                        "a float indicating the p-value cutoff to return the results (optional, otherwise all results returned), eg. 0.05"},
                                    'filter_type': {'cutoff', 'top_n'},
                                    'value': {
                                        'If `top_n` is set for `filter_type` this is an int indicating the top number (the smallest) of p-values to return. If instead `cutoff` is set then this is a float indicating the p-value cutoff to return the results. (optional, otherwise all results returned)'},
                                    }

        # A little function to describe what this thing does
        if describe:
            description_dict = self.command_definitions['fisher_exact_test']
            return description_dict

        # Make sure only allowable parameters and values have been passed
        self.check_params(allowable_parameters)
        # return if bad parameters have been passed
        if 'filter_type' in self.parameters and 'value' in self.parameters:
            if self.parameters['filter_type'] == 'cutoff':
                self.parameters['cutoff'] = self.parameters['value']
                if 'top_n' in self.parameters:
                    del self.parameters['top_n']
            elif self.parameters['filter_type'] == 'top_n':
                self.parameters['top_n'] = self.parameters['value']
                if type(self.parameters['top_n']) == float:
                    self.response.error(
                        f"Supplied value {self.parameters['top_n']} is not permitted. If 'top_n' is supplied for the 'filter_type' parameter, then the 'value' parameter cannot be a float it must be an integer or None.",
                        error_code="UnknownValue")
                if 'cutoff' in self.parameters:
                    del self.parameters['cutoff']
        if self.response.status != 'OK':
            return self.response

        # now do the call out to FTEST
        from Overlay.fisher_exact_test import ComputeFTEST
        FTEST = ComputeFTEST(self.response, self.message, self.parameters)
        response = FTEST.fisher_exact_test()
        return response

    def __overlay_exposures_data(self, describe=False):
        """
        This function applies the action overlay_exposures_data. It adds ICEES+ p-values either as virtual edges (if
        the virtual_relation_label, subject_qnode_key, and object_qnode_key are provided) or as EdgeAttributes tacked onto
        existing edges in the knowledge graph (applied to all edges).
        return: ARAXResponse
        """
        message = self.message
        parameters = self.parameters
        response = self.response

        # Make a list of the allowable parameters and their possible values
        if message and parameters and hasattr(message, 'query_graph') and hasattr(message.query_graph, 'edges'):
            allowable_parameters = {'action': {'overlay_exposures_data'},
                                    'virtual_relation_label': {self.parameters.get('virtual_relation_label')},
                                    'subject_qnode_key': {key for key in self.message.query_graph.nodes.keys()},
                                    'object_qnode_key': {key for key in self.message.query_graph.nodes.keys()}}
        else:
            allowable_parameters = {'action': {'overlay_exposures_data'},
                                    'virtual_relation_label': {
                                        'any string label used to identify the virtual edge (optional, otherwise information is added as an attribute to all existing edges in the KG)'},
                                    'subject_qnode_key': {
                                        'a specific subject query node id (optional, otherwise applied to all edges)'},
                                    'object_qnode_key': {
                                        'a specific object query node id (optional, otherwise applied to all edges)'}}

        # A little function to describe what this thing does
        if describe:
            description_dict = self.command_definitions['overlay_exposures_data']
            return description_dict

        # Make sure only allowable parameters and values have been passed
        self.check_params(allowable_parameters)
        if response.status != 'OK':
            return response

        from Overlay.overlay_exposures_data import OverlayExposuresData
        oed = OverlayExposuresData(response, message, parameters)
        response = oed.overlay_exposures_data()
        return response


##########################################################################################
def main():
    print("start ARAX_overlay")
    #### Note that most of this is just manually doing what ARAXQuery() would normally do for you

    #### Create a response object
    response = ARAXResponse()

    #### Create an ActionsParser object
    from actions_parser import ActionsParser
    actions_parser = ActionsParser()

    #### Set a simple list of actions
    # actions_list = [
    #    "overlay(compute_confidence_scores=true)",
    #    "return(message=true,store=false)"
    # ]

    actions_list = [
        # "overlay(action=compute_ngd)",
        # "overlay(action=compute_ngd, virtual_edge_type=NGD1, subject_qnode_key=n00, object_qnode_key=n01)",
        # "overlay(action=overlay_clinical_info, paired_concept_frequency=true)",
        # "overlay(action=overlay_clinical_info, paired_concept_frequency=true, virtual_edge_type=P1, subject_qnode_key=n00, object_qnode_key=n01)",
        # "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_edge_type=J1)",
        # "overlay(action=add_node_pmids)",
        # "overlay(action=overlay_clinical_info, observed_expected_ratio=true)",
        # "overlay(action=overlay_clinical_info, paired_concept_frequency=true, virtual_edge_type=P1, subject_qnode_key=n00, object_qnode_key=n01)",
        "overlay(action=predict_drug_treats_disease, subject_qnode_key=n01, object_qnode_key=n00, virtual_edge_type=P1)",
        "return(message=true,store=false)"
    ]

    #### Parse the action_list and print the result
    result = actions_parser.parse(actions_list)
    response.merge(result)
    if result.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response
    actions = result.data['actions']

    #### Read message #2 from the database. This should be the acetaminophen proteins query result message
    sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../../UI/Feedback")
    from RTXFeedback import RTXFeedback
    araxdb = RTXFeedback()

    # message_dict = araxdb.getMessage(2)  # acetaminophen2proteins graph
    # message_dict = araxdb.getMessage(13)  # ibuprofen -> proteins -> disease # work computer
    # message_dict = araxdb.getMessage(14)  # pleuropneumonia -> phenotypic_feature # work computer
    # message_dict = araxdb.getMessage(16)  # atherosclerosis -> phenotypic_feature  # work computer
    # message_dict = araxdb.getMessage(5)  # atherosclerosis -> phenotypic_feature  # home computer
    # message_dict = araxdb.getMessage(10)
    # message_dict = araxdb.getMessage(36)  # test COHD obs/exp, via ARAX_query.py 16
    # message_dict = araxdb.getMessage(39)  # ngd virtual edge test
    message_dict = araxdb.getMessage(1)

    #### The stored message comes back as a dict. Transform it to objects
    from ARAX_messenger import ARAXMessenger
    message = ARAXMessenger().from_dict(message_dict)
    # print(json.dumps(message.to_dict(),sort_keys=True,indent=2))

    #### Create an overlay object and use it to apply action[0] from the list
    print("Applying action")
    overlay = ARAXOverlay()
    result = overlay.apply(message, actions[0]['parameters'])
    response.merge(result)
    print("Finished applying action")

    # if result.status != 'OK':
    #    print(response.show(level=ARAXResponse.DEBUG))
    #    return response
    # response.data = result.data

    #### If successful, show the result
    # print(response.show(level=ARAXResponse.DEBUG))
    # response.data['message_stats'] = { 'n_results': message.n_results, 'id': message.id,
    #    'reasoner_id': message.reasoner_id, 'tool_version': message.tool_version }
    # response.data['message_stats']['confidence_scores'] = []
    # for result in message.results:
    #    response.data['message_stats']['confidence_scores'].append(result.confidence)

    # print(json.dumps(ast.literal_eval(repr(response.data['parameters'])),sort_keys=True,indent=2))
    # print(json.dumps(ast.literal_eval(repr(response.data['message_stats'])),sort_keys=True,indent=2))
    # a comment on the end so you can better see the network on github

    # look at the response
    # print(response.show(level=ARAXResponse.DEBUG))
    # print(response.show())
    # print("Still executed")

    # look at the edges
    # print(json.dumps(ast.literal_eval(repr(message.knowledge_graph.edges.values())),sort_keys=True,indent=2))
    # print(json.dumps(ast.literal_eval(repr(message.knowledge_graph.nodes.values())), sort_keys=True, indent=2))
    # print(json.dumps(message.to_dict(), sort_keys=True, indent=2))
    # print(response.show(level=ARAXResponse.DEBUG))

    # just print off the values
    # print(json.dumps(ast.literal_eval(repr(message.knowledge_graph.edges.values())), sort_keys=True, indent=2))
    # for edge in message.knowledge_graph.edges.values():
    #    if hasattr(edge, 'attributes') and edge.attributes and len(edge.attributes) >= 1:
    #        print(edge.attributes.pop().value)
    # print(f"Message: {json.dumps(message.to_dict(), sort_keys=True, indent=2)}")
    # print(message)
    print(
        f"KG edges: {json.dumps(ast.literal_eval(repr(message.knowledge_graph.edges.values())), sort_keys=True, indent=2)}")
    # print(response.show(level=ARAXResponse.DEBUG))
    print("Yet you still got here")
    # print(actions_parser.parse(actions_list))


if __name__ == "__main__": main()

# code/ARAX/ARAXQuery/ARAX_query.py

#!/bin/env python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast
import re
import time
from datetime import datetime
import subprocess
import traceback
from collections import Counter
import numpy as np
import threading
import json
import uuid
import requests
import gc
import contextlib
import connexion

from ARAX_response import ARAXResponse
from query_graph_info import QueryGraphInfo
from knowledge_graph_info import KnowledgeGraphInfo
from actions_parser import ActionsParser
from ARAX_filter import ARAXFilter
from ARAX_resultify import ARAXResultify
from ARAX_query_graph_interpreter import ARAXQueryGraphInterpreter
from ARAX_messenger import ARAXMessenger
from ARAX_ranker import ARAXRanker
from operation_to_ARAXi import WorkflowToARAXi
from ARAX_query_tracker import ARAXQueryTracker

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../UI/OpenAPI/python-flask-server/")
from openapi_server.models.response import Response
from openapi_server.models.message import Message
from openapi_server.models.knowledge_graph import KnowledgeGraph
from openapi_server.models.query_graph import QueryGraph
from openapi_server.models.q_node import QNode
from openapi_server.models.q_edge import QEdge
from openapi_server.models.operations import Operations

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../..")
from RTXConfiguration import RTXConfiguration

from openapi_server.models.message import Message
from openapi_server.models.q_node import QNode
from openapi_server.models.q_edge import QEdge

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../reasoningtool/QuestionAnswering")

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ResponseCache")
from response_cache import ResponseCache

from ARAX_database_manager import ARAXDatabaseManager
from reasoner_validator import validate
from jsonschema.exceptions import ValidationError

ARAXResponse.output = 'STDERR'

null_context_manager = contextlib.nullcontext()

query_tracker_reset = ARAXQueryTracker()
query_tracker_reset.clear_unfinished_entries()

class response_locking(ARAXResponse):
    def __init__(self, lock: threading.Lock):
        self.lock = lock
        super().__init__()

    def __add_message(self, message, level, code=None):
        with self.lock:
            super()._add_message(message, level, code)

class ARAXQuery:

    #### Constructor
    def __init__(self):
        self.response = None
        self.message = None
        self.rtxConfig = RTXConfiguration()
        self.DBManager = ARAXDatabaseManager()
        self.lock = None
        if self.DBManager.check_versions():
            self.response = ARAXResponse()
            self.response.debug(f"At least one database file is either missing or out of date. Updating now... (This may take a while)")
            self.response = self.DBManager.update_databases(True, response=self.response)


    def handle_memory_error(self, e):
        with self.lock if self.lock is not None else null_context_manager:
            self.response.envelope.message.results = []
        gc.collect()
        print("[asynchronous_query]: " + repr(e), file=sys.stderr)
        with self.lock if self.lock is not None else null_context_manager:
            self.response.error("ARAX ran out of memory during query processing; no results will be returned for this query")


    def query_return_stream(self, query, mode='ARAX'):

        main_query_thread = threading.Thread(target=self.asynchronous_query, args=(query,mode,))
        self.lock = threading.Lock()
        main_query_thread.start()

        if self.response is None or "DONE" not in self.response.status:

            # Sleep until a response object has been created
            have_response = False
            while not have_response:
                with self.lock:
                    have_response = (self.response is not None)
                if not have_response:
                    time.sleep(0.1)

            try:
                i_message = 0
                with self.lock:
                    n_messages = len(self.response.messages)
                query_plan_counter = 0
                idle_ticks = 0.0
                pid = None

                self.response.debug("In query_return_stream")

                response_status_says_done = False
                while not response_status_says_done:
                    with self.lock:
                        response_status_says_done = ("DONE" in self.response.status)
                    if response_status_says_done:
                        break
                    with self.lock:
                        n_messages = len(self.response.messages)
                    while i_message < n_messages:
                        with self.lock:
                            i_message_obj = self.response.messages[i_message].copy()
                        yield(json.dumps(i_message_obj) + "\n")
                        i_message += 1
                        idle_ticks = 0.0

                    if pid is None:
                        pid = os.getpid()
                        authorization = str(hash('Pickles' + str(pid)))
                        yield(json.dumps( { "pid": pid, "authorization": authorization } )+"\n")

                    #### Also emit any updates to the query_plan
                    with self.lock:
                        self_query_plan_counter = self.response.query_plan['counter']
                    if query_plan_counter < self_query_plan_counter:
                        query_plan_counter = self_query_plan_counter
                        with self.lock:
                            self_response_query_plan = self.response.query_plan.copy()
                        yield(json.dumps(self_response_query_plan, sort_keys=True) + "\n")
                        idle_ticks = 0.0
                    time.sleep(0.2)
                    idle_ticks += 0.2
                    if idle_ticks > 180.0:
                        timestamp = str(datetime.now().isoformat())
                        yield json.dumps({ 'timestamp': timestamp, 'level': 'DEBUG', 'code': '', 'message': 'Query is still progressing...' }) + "\n"
                        idle_ticks = 0.0
            except MemoryError as e:
                self.handle_memory_error(e)

                # #### If there are any more logging messages in the queue, send them first
            n_messages = len(self.response.messages)
            while i_message < n_messages:
                yield(json.dumps(self.response.messages[i_message]) + "\n")
                i_message += 1

            #### Also emit any updates to the query_plan
            self_response_query_plan_counter = self.response.query_plan['counter']
            if query_plan_counter < self_response_query_plan_counter:
                query_plan_counter = self_response_query_plan_counter
                yield(json.dumps(self.response.query_plan, sort_keys=True) + "\n")

            # Remove the little DONE flag the other thread used to signal this thread that it is done
            self.response.status = re.sub('DONE,', '', self.response.status)

            # Stream the resulting message back to the client
            yield(json.dumps(self.response.envelope.to_dict(), sort_keys=True) + "\n")

        # Wait until both threads rejoin here and the return
        main_query_thread.join()
        self.track_query_finish()
        return


    def asynchronous_query(self,query, mode='ARAX'):

        try:
            #### Define a new response object if one does not yet exist
            with self.lock:
                have_response = self.response is not None
            if not have_response:
                new_response = response_locking(self.lock)
                with self.lock:
                    self.response = new_response

            self.response.debug("in asynchronous_query")

            #### Execute the query
            self.query(query, mode=mode, origin='API')

        except MemoryError as e:
            self.handle_memory_error(e)


        # Insert a little flag into the response status to denote that this thread is done
        with self.lock:
            self.response.status = f"DONE,{self.response.status}"

        return


    ########################################################################################
    def query_return_message(self, query, mode='ARAX'):

        self.response = ARAXResponse()
        response = self.response
        print("in query_return_message - printing", file=sys.stderr)
        response.debug("in query_return_message")

        self.query(query, mode=mode, origin='API')

        #### If the query ended in an error, copy the error to the envelope
        if response.status != 'OK':
            response.envelope.status = response.error_code
            response.envelope.description = response.message
            if hasattr(response,'http_status'):
                response.envelope.http_status = response.http_status

        if mode == 'asynchronous':
            attributes = {
                'status': 'Running Async',
                'message_id': None,
                'message_code': 'Running',
                'code_description': 'Query running via /asyncquery'
            }
            query_tracker = ARAXQueryTracker()
            query_tracker.update_tracker_entry(self.response.tracker_id, attributes)
        else:
            self.track_query_finish()

        return response.envelope


    ########################################################################################
    def track_query_finish(self):

        query_tracker = ARAXQueryTracker()
        try:
            response_id = self.response.response_id
        except:
            response_id = None

        attributes = {
            'status': 'Completed',
            'message_id': response_id,
            'message_code': self.response.error_code,
            'code_description': self.response.message
        }

        query_tracker.update_tracker_entry(self.response.tracker_id, attributes)




    ########################################################################################
    def query(self, query, mode='ARAX', origin='local'):

        #### Create the skeleton of the response
        response = self.response
        if response is None:  # At this point in the code, the response should only be
                              # None in regression tests that call ARAXQuery.query() directly
            response = ARAXResponse()
            self.response = response

        #### Announce the launch of query()
        #### Note that setting ARAXResponse.output = 'STDERR' means that we get noisy output to the logs
        response.info(f"{mode} Query launching on incoming Query")
        response.debug(f"RTXConfiguration says maturity={self.rtxConfig.maturity}, "
                       f"current_branch={self.rtxConfig.current_branch_name}, "
                       f"is_itrb_instance={self.rtxConfig.is_itrb_instance}, "
                       f"arax_version={self.rtxConfig.arax_version}, "
                       f"trapi_version={self.rtxConfig.trapi_version}")

        #### Create an empty envelope
        messenger = ARAXMessenger()
        messenger.create_envelope(response)

        #### Preserve the query_options
        if 'query_options' in query and query['query_options'] is not None:
            response.envelope.query_options = query['query_options']
        else:
            response.envelope.query_options = {}

        #### Need to put certain input Query parameters into query_options to later use by Expand et al.
        if 'return_minimal_metadata' in query:
            response.envelope.query_options['return_minimal_metadata'] = query['return_minimal_metadata']

        #### If a submitter came in, reflect that back into the response
        if "callback" in query and query['callback'] is not None and query['callback'].startswith('http://localhost:8000/ars/'):
            response.envelope.submitter = 'ARS'
        elif 'submitter' in query:
            response.envelope.submitter = query['submitter']
        else:
            if "callback" in query and query['callback'] is not None:
                match = re.match(r'http[s]://(.+?)/',query['callback'])
                if match:
                    response.envelope.submitter = match.group(1)
                else:
                    response.envelope.submitter = '?'
            else:
                response.envelope.submitter = '?'

        #### Create an entry to track this query
        tracker_id = None
        if origin == 'API':
            query_tracker = ARAXQueryTracker()
            if 'remote_address' in query and query['remote_address'] is not None:
                remote_address = query['remote_address']
            else:
                remote_address = '????'
            attributes = { 'submitter': response.envelope.submitter, 'input_query': query, 'remote_address': remote_address }
            tracker_id = query_tracker.create_tracker_entry(attributes)
        response.tracker_id = tracker_id

        try:
            #### Determine a plan for what to do based on the input
            #eprint(json.dumps(query, indent=2, sort_keys=True))
            result = self.examine_incoming_query(query, mode=mode)
            if result.status != 'OK':
                return response
            query_attributes = result.data

            #### Convert the message from dicts to objects
            if 'message' in query:
                response.debug(f"Deserializing message")
                query['message'] = ARAXMessenger().from_dict(query['message'])

            # If there is a workflow, translate it to ARAXi and append it to the operations actions list
            if "have_workflow" in query_attributes:
                if query['message'].query_graph is None:
                    response.error(f"Cannot have a workflow with an null query_graph", error_code="MissingQueryGraph")
                    return response

                try:
                    self.convert_workflow_to_ARAXi(query)
                except Exception as error:
                    exception_type, exception_value, exception_traceback = sys.exc_info()
                    response.error(f"An unhandled error occurred: {error}: {repr(traceback.format_exception(exception_type, exception_value, exception_traceback))}", error_code="UnhandledError")
                    return response
                query_attributes["have_operations"] = True

            # #### If we have a query_graph in the input query
            if "have_query_graph" in query_attributes and "have_operations" not in query_attributes:

                response.envelope.message.query_graph = query['message'].query_graph

                #### In ARAX mode, run the QueryGraph through the QueryGraphInterpreter and to generate ARAXi
                if mode == 'ARAX' or mode == 'asynchronous':
                    response.info(f"Found input query_graph. Interpreting it and generating ARAXi processing plan to answer it")
                    interpreter = ARAXQueryGraphInterpreter()
                    interpreter.translate_to_araxi(response)
                    if response.status != 'OK':
                        return response
                    query['operations'] = {}
                    query['operations']['actions'] = result.data['araxi_commands']

                #### Else the mode is KG2 mode, where we just accept one-hop queries, and run a simple ARAXi
                else:
                    response.info(f"Found input query_graph. Querying RTX KG2 to answer it")
                    if len(response.envelope.message.query_graph.nodes) > 2:
                        response.error(f"Only 1 hop (2 node) queries can be handled at this time", error_code="TooManyHops")
                        return response
                    query['operations'] = {}
                    query['operations']['actions'] = [ 'expand(kp=infores:rtx-kg2)', 'resultify()', 'return(store=false)' ]

                query_attributes['have_operations'] = True


            #### If we have operations, execute them
            if "have_operations" in query_attributes:
                response.info(f"Found input processing plan. Sending to the ProcessingPlanExecutor")
                result = self.execute_processing_plan(query, mode=mode)

            #### This used to support canned queries, but no longer does
            else:
                response.error(f"Unable to determine ARAXi to execute. Error Q213", error_code="UnknownError")

        except MemoryError as e:
            self.handle_memory_error(e)

        return response


    #######################################################################################
    def examine_incoming_query(self, query, mode='ARAX'):

        response = self.response
        response.info(f"Examine input Query for needed information for dispatch")
        #eprint(query)

        #### Check to see if there's an operations processing plan
        if 'operations' in query and query['operations'] is not None:
            response.data["have_operations"] = 1

        #### Check to see if there's a workflow processing plan
        if 'workflow' in query and query['workflow'] is not None:
            response.data["have_workflow"] = 1

        #### Check to see if there's a query message to process
        if 'message' in query and query['message'] is not None:
            response.data["have_message"] = 1

            #### Check if there is a query_graph
            if "query_graph" in query["message"] and query["message"]["query_graph"] is not None:
                response.data["have_query_graph"] = 1
                self.validate_incoming_query_graph(query["message"])

        #### Check to see if there is at least a message or a operations
        if "have_message" not in response.data and "have_operations" not in response.data:
            response.error("No message or operations present in Query", error_code="NoQueryMessageOrOperations")
            return response

        # #### FIXME Need to do more validation and tidying of the incoming message here or somewhere


        # RTXKG2 does not support operations
        if mode == 'RTXKG2' and "have_operations" in response.data:
            response.error("RTXKG2 does not support operations in Query", error_code="OperationsNotSupported")
            return response

        #### If we got this far, then everything seems to be good enough to proceed
        return response


    #######################################################################################
    def convert_workflow_to_ARAXi(self, query):

        response = self.response
        response.info(f"Converting workflow elements to ARAXi")

        # Convert the TRAPI workflow into ARAXi
        converter = WorkflowToARAXi()
        araxi = converter.translate(query['workflow'], query['message'].query_graph.to_dict(), response)
        # The translation returns a list of ARAXi commands. If this list is empty, something went wrong
        # When convert_workflow_to_ARAXi is called, it's wrapped in a try/except, so raise an Exception to indicate
        # that something went wrong
        if not araxi:
            response.error("Unable to translate workflow into ARAXi", error_code="TranslationFailed")
            raise Exception

        # If there are not already operations, create empty stubs
        if 'operations' not in query:
            query['operations'] = {}
        if 'actions' not in query['operations']:
            query['operations']['actions'] = []

        # Append the new workflow-based ARAXi onto the end of the existing actions if there are any
        query['operations']['actions'].extend(araxi)

        return response


    ############################################################################################
    def validate_incoming_query_graph(self,message):

        response = self.response
        response.info(f"Validating the input query graph")

        # Define allowed qnode and qedge attributes to check later
        allowed_qnode_attributes = { 'ids': 1, 'categories':1, 'is_set': 1, 'option_group_id': 1, 'name': 1, 'constraints': 1 }
        allowed_qedge_attributes = { 'predicates':1, 'subject': 1, 'object': 1, 'option_group_id': 1, 'exclude': 1, 'relation': 1, 'attribute_constraints': 1, 'qualifier_constraints': 1, 'knowledge_type': 1 }

        #### Loop through nodes checking the attributes
        for id,qnode in message['query_graph']['nodes'].items():
            for attr in qnode:
                if attr not in allowed_qnode_attributes:
                    response.error(f"QueryGraph node '{id}' has an unexpected property '{attr}'. This property is not understood and therefore processing is halted, rather than answer an incompletely understood query", error_code="UnknownQNodeProperty")
                    return response

        #### Loop through edges checking the attributes
        for id,qedge in message['query_graph']['edges'].items():
            for attr in qedge:
                if attr not in allowed_qedge_attributes:
                    response.error(f"QueryGraph edge '{id}' has an unexpected property '{attr}'. This property is not understood and therefore processing is halted, rather than answer an incompletely understood query", error_code="UnknownQEdgeProperty")
                    return response

        return response


    ############################################################################################
    def limit_message(self,message,query):
        if "max_results" in query and query["max_results"] is not None:
            if message.results is not None:
                if len(message.results) > query["max_results"]:
                    del message.results[query["max_results"]:]
                    message.code_description += " (output is limited to "+str(query["max_results"]) + " results)"



    ############################################################################################
    #### Given an input query with a processing plan, execute that processing plan on the input
    def execute_processing_plan(self,input_operations_dict, mode='ARAX'):

        response = self.response
        response.debug(f"Entering execute_processing_plan")
        messages = []
        message = None

        # If there is already a message (perhaps with a query_graph) already in the query, preserve it
        if 'message' in input_operations_dict and input_operations_dict['message'] is not None:
            incoming_message = input_operations_dict['message']
            if isinstance(incoming_message,dict):
                incoming_message = Message.from_dict(incoming_message)
            messages = [ incoming_message ]

        #### Pull out the main processing plan
        operations = Operations.from_dict(input_operations_dict["operations"])

        #### Connect to the message store just once, even if we won't use it
        response_cache = ResponseCache()
        response_cache.connect()

        #### Create a messenger object for basic message processing
        messenger = ARAXMessenger()

        #### If there are URIs provided, try to load them
        force_remote = False
        if operations.message_uris is not None:
            response.debug(f"Found message_uris")
            for uri in operations.message_uris:
                response.debug(f"    messageURI={uri}")
                matchResult = re.match( r'http[s]://arax.ncats.io/.*api/arax/.+/response/(\d+)',uri,re.M|re.I )
                if matchResult and not force_remote:
                    referenced_response_id = matchResult.group(1)
                    response.debug(f"Found local ARAX identifier corresponding to response_id {referenced_response_id}")
                    response.debug(f"Loading response_id {referenced_response_id}")
                    referenced_envelope = response_cache.get_response(referenced_response_id)

                    if False:
                        #### Hack to get it to work
                        for node_key,node in referenced_envelope["message"]["knowledge_graph"]["nodes"].items():
                            if 'attributes' in node and node['attributes'] is not None:
                                new_attrs = []
                                for attr in node['attributes']:
                                    if attr['type'] is not None:
                                        new_attrs.append(attr)
                                if len(new_attrs) < len(node['attributes']):
                                    node['attributes'] = new_attrs

                        #### Hack to get it to work
                        for node_key,node in referenced_envelope["message"]["knowledge_graph"]["edges"].items():
                            if 'attributes' in node and node['attributes'] is not None:
                                new_attrs = []
                                for attr in node['attributes']:
                                    if attr['type'] is not None:
                                        new_attrs.append(attr)
                                if len(new_attrs) < len(node['attributes']):
                                    node['attributes'] = new_attrs

                    if isinstance(referenced_envelope,dict):
                        referenced_envelope = Response().from_dict(referenced_envelope)
                        #messages.append(referenced_message)
                        messages = [ referenced_envelope.message ]
                        #eprint(json.dumps(referenced_envelope.message.results,indent=2))
                    else:
                        response.error(f"Unable to load response_id {referenced_response_id}", error_code="CannotLoadPreviousResponseById")
                        return response

                else:
                    loaded_message = messenger.fetch_message(uri)
                    messages = [ loaded_message ]

        #### If there are one or more messages embedded in the POST, process them
        if operations.messages is not None:
            response.debug(f"Received messages")
            for uploadedMessage in operations.messages:
                response.debug(f"uploadedMessage is a "+str(uploadedMessage.__class__))
                if str(uploadedMessage.__class__) == "<class 'openapi_server.models.message.Message'>":
                    uploadedMessage = ARAXMessenger().from_dict(uploadedMessage)
                    messages.append(uploadedMessage)

                    if uploadedMessage.results:
                        pass
                        #if message["terms"] is None:
                        #    message["terms"] = { "dummyTerm": "giraffe" }
                        #if message["query_type_id"] is None:
                        #    message["query_type_id"] = "UnknownQ"
                        #if message["restated_question"] is None:
                        #    message["restated_question"] = "Unknown question"
                        #if message["original_question"] is None:
                        #    message["original_question"] = "Unknown question"

                        #query = { "query_type_id": message["query_type_id"], "restated_question": message["restated_question"], "original_question": message["original_question"], "terms": message["terms"] }
                    else:
                        #response.error(f"Uploaded message does not contain a results. May be the wrong format")
                        #return response
                        response.warning(f"There are no results in this uploaded message, but maybe that's okay")
                else:
                    response.error(f"Uploaded message is not of type Message. It is of type"+str(uploadedMessage.__class__))
                    return response

        #### Take different actions based on the number of messages we now have in hand
        n_messages = len(messages)

        #### If there's no input message, then create one
        if n_messages == 0:
            response.debug(f"No starting messages were referenced. Will start with a blank template Message")
            messenger.create_envelope(response)

            message = response.envelope.message

        #### If there's on message, we will run with that
        elif n_messages == 1:
            response.debug(f"A single Message is ready and in hand")
            message = messages[0]
            response.envelope.message = message

        #### Multiple messages unsupported
        else:
            response.debug(f"Multiple Messages were uploaded or imported by reference. However, proper merging code has not been implmented yet! Will use just the first Message for now.")
            message = messages[0]

        #### Examine the options that were provided and act accordingly
        optionsDict = {}
        if operations.options:
            response.debug(f"Processing options were provided, but these are not implemented at the moment and will be ignored")
            for option in operations.options:
                response.debug(f"   option="+option)
                optionsDict[option] = 1


        #### If there are actions, then fulfill those
        if operations.actions:
            response.debug(f"Found actions")
            actions_parser = ActionsParser()
            result = actions_parser.parse(operations.actions)
            response.merge(result)
            if result.error_code != 'OK':
                return response

            #### Put our input processing actions into the envelope
            if response.envelope.operations is None:
                response.envelope.operations = {}
            response.envelope.operations['actions'] = operations.actions


            #### Import the individual ARAX processing modules and process DSL commands
            from ARAX_expander import ARAXExpander
            from ARAX_overlay import ARAXOverlay
            from ARAX_filter_kg import ARAXFilterKG
            from ARAX_resultify import ARAXResultify
            from ARAX_filter_results import ARAXFilterResults
            from ARAX_infer import ARAXInfer
            from ARAX_connect import ARAXConnect
            expander = ARAXExpander()
            filter = ARAXFilter()
            overlay = ARAXOverlay()
            filter_kg = ARAXFilterKG()
            resultifier = ARAXResultify()
            filter_results = ARAXFilterResults()
            infer = ARAXInfer()
            connect = ARAXConnect()
            self.message = message

            #### Create some empty stubs if they don't exist
            if message.results is None:
                message.results = []


            #### If the mode is asynchronous, then fork here. The parent returns the response thus far that everything checks out and is proceeding
            #### and the child continues to work on the query, eventually to finish and exit()
            if mode == 'asynchronous':
                callback = input_operations_dict['callback']
                response.info(f"Everything seems in order to begin processing the query asynchronously. Processing will continue and Response will be posted to {callback}")
                newpid = os.fork()
                #### The parent returns to tell the caller that work will proceed
                if newpid > 0:
                    response.envelope.status = 'Running'
                    response.envelope.description = 'Asynchronous answering of query underway'
                    return response
                #### The child continues
                #### The child loses the MySQL connection of the parent, so need to reconnect
                response_cache.connect()
                time.sleep(1)
                attributes = {
                    'pid': os.getpid(),
                    'code_description': 'Query executing via /asyncquery'
                }
                query_tracker = ARAXQueryTracker()
                query_tracker.alter_tracker_entry(self.response.tracker_id, attributes)


            #### If there is already a KG with edges, recompute the qg_keys
            if message.knowledge_graph is not None and len(message.knowledge_graph.edges) > 0:
                resultifier.recompute_qg_keys(response)

            #### Process each action in order
            action_stats = { }
            actions = result.data['actions']
            action = None
            for action in actions:
                response.info(f"Processing action '{action['command']}' with parameters {action['parameters']}")
                nonstandard_result = False
                skip_merge = False

                # Catch a crash
                try:
                    if action['command'] == 'create_message':
                        messenger.create_envelope(response)
                        #### Put our input processing actions into the envelope
                        if response.envelope.query_options is None:
                            response.envelope.query_options = {}
                        response.envelope.query_options['actions'] = operations.actions

                    elif action['command'] == 'fetch_message':
                        messenger.apply_fetch_message(response,action['parameters'])

                    elif action['command'] == 'add_qnode':
                        messenger.add_qnode(response,action['parameters'])

                    elif action['command'] == 'add_qedge':
                        messenger.add_qedge(response,action['parameters'])

                    elif action['command'] == 'expand':
                        self.inject_int_value_into_parameters('kp_timeout', response.envelope.query_options, action['parameters'], 'UserTimeoutNotInt')
                        self.inject_int_value_into_parameters('prune_threshold', response.envelope.query_options, action['parameters'], 'PruneThresholdNotInt')
                        self.inject_boolean_value_into_parameters('return_minimal_metadata', response.envelope.query_options, action['parameters'], 'InternalError')
                        if response.status == 'ERROR':
                            if mode == 'asynchronous':
                                self.send_to_callback(callback, response)
                            return response
                        expander.apply(response, action['parameters'], mode=mode)

                    elif action['command'] == 'filter':
                        filter.apply(response,action['parameters'])

                    elif action['command'] == 'resultify':
                        resultifier.apply(response, action['parameters'])

                    elif action['command'] == 'scoreless_resultify':
                        resultifier.apply(response, action['parameters'])

                    elif action['command'] == 'overlay':  # recognize the overlay command
                        overlay.apply(response, action['parameters'])

                    elif action['command'] == 'filter_kg':  # recognize the filter_kg command
                        filter_kg.apply(response, action['parameters'])

                    elif action['command'] == 'infer':  # recognize the infer command
                        infer.apply(response, action['parameters'])

                    elif action['command'] == 'filter_results':  # recognize the filter_results command
                        response.debug(f"Before filtering, there are {len(response.envelope.message.results)} results")
                        filter_results.apply(response, action['parameters'])

                    elif action['command'] == 'query_graph_reasoner':
                        response.info(f"Sending current query_graph to the QueryGraphReasoner")
                        qgr = QueryGraphReasoner()
                        message = qgr.answer(ast.literal_eval(repr(message.query_graph)), TxltrApiFormat=True)
                        self.message = message
                        nonstandard_result = True

                    elif action['command'] == 'connect':
                        connect.apply(response, action['parameters'])

                    elif action['command'] == 'return':
                        action_stats['return_action'] = action
                        break

                    elif action['command'] == 'rank_results':
                        response.info(f"Running experimental reranker on results")
                        try:
                            ranker = ARAXRanker()
                            ranker.aggregate_scores_dmk(response)
                        except Exception as error:
                            exception_type, exception_value, exception_traceback = sys.exc_info()
                            response.error(f"An uncaught error occurred: {error}: {repr(traceback.format_exception(exception_type, exception_value, exception_traceback))}", error_code="UncaughtARAXiError")
                            if mode == 'asynchronous':
                                self.send_to_callback(callback, response)
                            return response

                    else:
                        response.error(f"Unrecognized command {action['command']}", error_code="UnrecognizedCommand")
                        if mode == 'asynchronous':
                            self.send_to_callback(callback, response)
                        return response

                except Exception as error:
                    exception_type, exception_value, exception_traceback = sys.exc_info()
                    response.error(f"An uncaught error occurred: {error}: {repr(traceback.format_exception(exception_type, exception_value, exception_traceback))}", error_code="UncaughtARAXiError")
                    if mode == 'asynchronous':
                        self.send_to_callback(callback, response)
                    return response

                #### If we're in an error state return now
                if response.status != 'OK':
                    response.envelope.status = response.error_code
                    response.envelope.description = response.message
                    if mode == 'asynchronous':
                        self.send_to_callback(callback, response)
                    return response

                #### Immediately after resultify, run the experimental ranker
                if action['command'] == 'resultify':
                    response.info(f"Running experimental reranker on results")
                    try:
                        ranker = ARAXRanker()
                        ranker.aggregate_scores_dmk(response)
                    except Exception as error:
                        exception_type, exception_value, exception_traceback = sys.exc_info()
                        response.error(f"An uncaught error occurred: {error}: {repr(traceback.format_exception(exception_type, exception_value, exception_traceback))}", error_code="UncaughtARAXiError")
                        if mode == 'asynchronous':
                            self.send_to_callback(callback, response)
                        return response

            #### At the end, process the explicit return() action, or implicitly perform one
            return_action = { 'command': 'return', 'parameters': { 'response': 'true', 'store': 'true' } }
            if action is not None and action['command'] == 'return':
                return_action = action
                #### If an explicit one left out some parameters, set the defaults
                if 'store' not in return_action['parameters']:
                    return_action['parameters']['store'] = 'false'
                if 'response' not in return_action['parameters']:
                    return_action['parameters']['response'] = 'false'

            #print(json.dumps(ast.literal_eval(repr(response.__dict__)), sort_keys=True, indent=2))
            #for node_key, node in response.envelope.message.knowledge_graph.nodes.items():
            #    if node.attributes is not None:
            #        for attr in node.attributes:
            #            eprint(f"  - {node_key}.{attr.name} is {type(attr.value)}")

            # Fill out the message with data
            response.envelope.status = response.error_code
            response.envelope.description = response.message
            if response.envelope.operations is None:
                response.envelope.operations = operations

            #### Provide the total results count in the Response if it is available
            try:
                response.envelope.total_results_count = response.total_results_count
            except:
                pass

            #response.envelope.operations['actions'] = operations.actions

            # Update the reasoner_id to ARAX if not already present
            for result in response.envelope.message.results:
                if result.reasoner_id is None:
                    result.reasoner_id = 'ARAX'

            # Store the validation and provenance metadata
            #trapi_version = '1.2.0'
            #try:
            #    validate(response.envelope,'Response',trapi_version)
            #    if 'description' not in response.envelope or response.envelope['description'] is None:
            #        response.envelope['description'] = 'reasoner-validator: PASS'
            #    response.envelope['validation_result'] = { 'status': 'PASS', 'version': trapi_version, 'message': '' }

            #except ValidationError as error:
            #    timestamp = str(datetime.now().isoformat())
            #    if 'logs' not in response.envelope or response.envelope['logs'] is None:
            #        response.envelope['logs'] = []
            #    response.envelope['logs'].append( { "code": 'InvalidTRAPI', "level": "ERROR", "message": "TRAPI validator reported an error: " + str(error),
            #                                        "timestamp": timestamp } )
            #    if 'description' not in reponse.envelope or response.envelope['description'] is None:
            #        response.envelope['description'] = ''
            #    response.envelope['description'] = 'ERROR: TRAPI validator reported an error: ' + str(error) + ' --- ' + response.envelope['description']
            #    response.envelope['validation_result'] = { 'status': 'FAIL', 'version': trapi_version, 'message': 'TRAPI validator reported an error: ' + str(error) + ' --- ' + response.envelope['description'] }


            #from ARAX_attribute_parser import ARAXAttributeParser
            #attribute_parser = ARAXAttributeParser(response.envelope,response.envelope['message'])
            #response.envelope.validation_result['provenance_summary'] = attribute_parser.summarize_provenance_info()
            #
            #response.envelope.validation_result = { 'status': 'PASS', 'version': trapi_version, 'size': '?', 'message': '' }
            #

            if response.envelope.query_options is None:
                response.envelope.query_options = {}
            response.envelope.query_options['query_plan'] = response.query_plan

            # If store=true, then put the message in the database
            response_id = None
            if return_action['parameters']['store'] == 'true':
                response.debug(f"Storing resulting Message")
                response_id = response_cache.add_new_response(response)
                response.info(f"Result was stored with id {response_id}. It can be viewed at https://arax.ncats.io/?r={response_id}")
            response.response_id = response_id

            #### Record how many results came back
            n_results = len(message.results)
            response.info(f"Processing is complete and resulted in {n_results} results.")
            if response.message == 'Normal completion':
                response.message = f"Normal completion with {n_results} results."

            #### If asking for the full message back
            if return_action['parameters']['response'] == 'true':
                if mode == 'asynchronous':
                    response.info(f"Processing is complete. Attempting to send the result to the callback URL.")
                    self.send_to_callback(callback, response)
                else:
                    response.info(f"Processing is complete. Transmitting resulting Message back to client.")
                    return response

            #### Else just the id is returned
            else:
                if response_id is None:
                    response_id = 0
                else:
                    response.info(f"Resulting Message id is {response_id} and is available to fetch via /response endpoint.")

                servername = 'localhost'
                if self.rtxConfig.is_production_server:
                    servername = 'arax.ncats.io'
                url = f"https://{servername}/api/arax/v1.0/response/{response_id}"

                return( { "status": 200, "response_id": str(response_id), "n_results": n_results, "url": url }, 200)



    ############################################################################################
    def send_to_callback(self, callback, response):
        response.info(f"Attempting to send to callback URL: {callback}")
        envelope_dict = response.envelope.to_dict()
        try:
            post_response_content = requests.post(callback, json=envelope_dict, headers={'accept': 'application/json'})
            status_code = post_response_content.status_code
            response.info(f"Response from POST to callback URL was {status_code}")
            if status_code not in [ 200, 201 ]:
                response.error(f"Response from POST to callback URL was {status_code}", error_code="UnreachableCallback")

        except:
            response.error(f"Unable to make a connection to URL {callback} at all. Work is lost", error_code="UnreachableCallback")
        self.track_query_finish()
        os._exit(0)


    ############################################################################################
    def inject_int_value_into_parameters(self, parameter_name, query_options, parameters, error_code):
        parameter_value = None
        if query_options is not None and parameter_name in query_options and query_options[parameter_name] is not None:
            parameter_value = query_options[parameter_name]
            #### Try to convery the value to an integer
            try:
                parameter_value = int(parameter_value)
            except:
                self.response.error(f"Unable to convert parameter {parameter_name} = '{parameter_value}' into an integer", error_code=error_code)
                return
        #### Only update the value in parameters if one was not explicitly specified
        if parameter_name not in parameters and parameter_value is not None:
            parameters[parameter_name] = parameter_value


    ############################################################################################
    def inject_boolean_value_into_parameters(self, parameter_name, query_options, parameters, error_code):
        parameter_value = False
        if query_options is not None and parameter_name in query_options and query_options[parameter_name] is not None:
            if query_options[parameter_name] is True:
                parameter_value = True
        parameters[parameter_name] = parameter_value


##################################################################################################
def stringify_dict(inputDict):
    outString = "{"
    for key,value in sorted(inputDict.items(), key=lambda t: t[0]):
        if outString != "{":
            outString += ","
        outString += "'"+str(key)+"':'"+str(value)+"'"
    outString += "}"
    return(outString)


##################################################################################################
def main():

    #### Parse command line options
    import argparse
    argparser = argparse.ArgumentParser(description='Primary interface to the ARAX system')
    argparser.add_argument('--verbose', action='count', help='If set, print more information about ongoing processing' )
    argparser.add_argument('example_number', type=int, help='Integer number of the example query to execute')
    params = argparser.parse_args()

    #### Set verbose
    verbose = params.verbose
    if verbose is None: verbose = 1

    #### Create the ARAXQuery object
    araxq = ARAXQuery()

    #### For debugging purposes, you can send all messages as they are logged to STDERR
    #ARAXResponse.output = 'STDERR'

    #### Set the query based on the supplied example_number
    if params.example_number == 0:
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=acetaminophen, key=n0)",
            "add_qnode(categories=biolink:Protein, key=n1)",
            "add_qedge(subject=n0, object=n1, key=e0)",
            "expand(edge_key=e0)",
            "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n0, object_qnode_key=n1)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=limit_number_of_results, max_results=10)",
            "return(message=true, store=true)",
        ]}}

    elif params.example_number == 1:
        query = { 'message': { 'query_type_id': 'Q0', 'terms': { 'term': 'lovastatin' } } }
        #query = { "query_type_id": "Q0", "terms": { "term": "lovastatin" }, "bypass_cache": "true" }  # Use bypass_cache if the cache if bad for this question

    elif params.example_number == 2:
        query = { "message": { "query_graph": { "edges": [
                    { "id": "qg2", "subject": "qg1", "object": "qg0", "type": "physically_interacts_with" }
                ],
                "nodes": [
                    { "id": "qg0", "name": "acetaminophen", "curie": "CHEMBL.COMPOUND:CHEMBL112", "type": "chemical_substance" },
                    { "id": "qg1", "name": None, "desc": "Generic protein", "curie": None, "type": "protein" }
                ] } } }

    elif params.example_number == 3:  # FIXME: Don't fix me, this is our planned demo example 1.
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=acetaminophen, key=n0)",
            "add_qnode(categories=biolink:Protein, key=n1)",
            "add_qedge(subject=n0, object=n1, key=e0)",
            "expand(edge_key=e0)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=limit_number_of_results, max_results=10)",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 301:  # Variant of 3 with NGD
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=acetaminophen, key=n0)",
            "add_qnode(categories=biolink:Protein, key=n1)",
            "add_qedge(subject=n0, object=n1, key=e0)",
            "expand(edge_key=e0)",
            "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n0, object_qnode_key=n1)",
            "resultify(ignore_edge_direction=true)",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 4:
        query = { "operations": { "actions": [
            "add_qnode(name=hypertension, key=n00)",
            "add_qnode(categories=biolink:Protein, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00)",
            "resultify()",
            "return(message=true, store=false)",
            ] } }
    elif params.example_number == 5:  # test overlay with ngd: hypertension->protein
        query = { "operations": { "actions": [
            "add_qnode(name=hypertension, key=n00)",
            "add_qnode(categories=biolink:Protein, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00)",
            "overlay(action=compute_ngd)",
            "resultify()",
            "return(message=true, store=true)",
            ] } }
    elif params.example_number == 6:  # test overlay
        query = { "operations": { "actions": [
            "create_message",
            "add_qnode(ids=DOID:12384, key=n00)",
            "add_qnode(categories=biolink:PhenotypicFeature, is_set=True, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00, type=has_phenotype)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            #"overlay(action=overlay_clinical_info, paired_concept_frequency=true)",
            #"overlay(action=overlay_clinical_info, chi_square=true, virtual_relation_label=C1, subject_qnode_key=n00, object_qnode_key=n01)",
            "overlay(action=overlay_clinical_info, paired_concept_frequency=true, virtual_relation_label=C1, subject_qnode_key=n00, object_qnode_key=n01)",
            #"overlay(action=compute_ngd, default_value=inf)",
            #"overlay(action=compute_ngd, virtual_relation_label=NGD1, subject_qnode_key=n00, object_qnode_key=n01)",
            "filter(maximum_results=2)",
            "return(message=true, store=true)",
            ] } }
    elif params.example_number == 7:  # stub to test out the compute_jaccard feature
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=DOID:14330, key=n00)",  # parkinsons
            "add_qnode(categories=biolink:Protein, is_set=True, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, is_set=false, key=n02)",
            "add_qedge(subject=n01, object=n00, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01)",
            "expand(edge_id=[e00,e01])",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
            "resultify()",
            "filter_results(action=limit_number_of_results, max_results=50)",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 8:  # to test jaccard with known result  # FIXME:  ERROR: Node DOID:8398 has been returned as an answer for multiple query graph nodes (n00, n02)
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=DOID:8398, key=n00)",  # osteoarthritis
            "add_qnode(categories=biolink:PhenotypicFeature, is_set=True, key=n01)",
            "add_qnode(type=disease, is_set=true, key=n02)",
            "add_qedge(subject=n01, object=n00, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01)",
            "expand(edge_id=[e00,e01])",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 9:  # to test jaccard with known result. This check's out by comparing with match p=(s:disease{ids:"DOID:1588"})-[]-(r:protein)-[]-(:chemical_substance) return p and manually counting
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=DOID:1588, key=n00)",
            "add_qnode(categories=biolink:Protein, is_set=True, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n02)",
            "add_qedge(subject=n01, object=n00, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01)",
            "expand(edge_id=[e00,e01])",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 10:  # test case of drug prediction
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=DOID:1588, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, is_set=false, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00)",
            "overlay(action=predict_drug_treats_disease)",
            "resultify(ignore_edge_direction=True)",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 11:  # test overlay with overlay_clinical_info, paired_concept_frequency via COHD
        query = { "operations": { "actions": [
            "create_message",
            "add_qnode(ids=DOID:0060227, key=n00)",  # Adam's oliver
            "add_qnode(categories=biolink:PhenotypicFeature, is_set=True, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00, type=has_phenotype)",
            "expand(edge_key=e00)",
            "overlay(action=overlay_clinical_info, paired_concept_frequency=true)",
            #"overlay(action=overlay_clinical_info, paired_concept_frequency=true, virtual_relation_label=COHD1, subject_qnode_key=n00, object_qnode_key=n01)",
            "filter(maximum_results=2)",
            "return(message=true, store=true)",
            ] } }
    elif params.example_number == 12:  # dry run of example 2 # FIXME NOTE: this is our planned example 2 (so don't fix, it's just so it's highlighted in my IDE)
        query = { "operations": { "actions": [
            "create_message",
            "add_qnode(name=DOID:14330, key=n00)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01, type=physically_interacts_with)",
            "expand(edge_id=[e00,e01], kp=ARAX/KG1)",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=jaccard_index, direction=below, threshold=.2, remove_connected_nodes=t, qnode_key=n02)",
            "filter_kg(action=remove_edges_by_property, edge_property=provided_by, property_value=Pharos)",  # can be removed, but shows we can filter by Knowledge provider
            "overlay(action=predict_drug_treats_disease, subject_qnode_key=n02, object_qnode_key=n00, virtual_relation_label=P1)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_edge_attribute, edge_attribute=jaccard_index, direction=descending, max_results=15)",
            "return(message=true, store=true)",
            ] } }
    elif params.example_number == 13:  # add pubmed id's
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:1227, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00)",
            "overlay(action=add_node_pmids, max_num=15)",
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 14:  # test
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:8712, key=n00)",
            "add_qnode(categories=biolink:PhenotypicFeature, is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n02)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n03)",
            "add_qedge(subject=n00, object=n01, key=e00, type=has_phenotype)",  # phenotypes of disease
            "add_qedge(subject=n02, object=n01, key=e01, type=indicated_for)",  # only look for drugs that are indicated for those phenotypes
            "add_qedge(subject=n02, object=n03, key=e02)",  # find proteins that interact with those drugs
            "expand(edge_id=[e00, e01, e02])",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",  # only look at drugs that target lots of phenotypes
            #"filter_kg(action=remove_edges_by_attribute, edge_attribute=jaccard_index, direction=below, threshold=.06, remove_connected_nodes=t, qnode_key=n02)",  # remove edges and drugs that connect to few phenotypes
            #"filter_kg(action=remove_edges_by_type, edge_type=J1, remove_connected_nodes=f)",
            ##"overlay(action=overlay_clinical_info, paired_concept_frequency=true)",  # overlay with COHD information
            #"overlay(action=overlay_clinical_info, paired_concept_frequency=true, virtual_relation_label=C1, subject_qnode_key=n00, object_qnode_key=n02)",  # overlay drug->disease virtual edges with COHD information
            #"filter_kg(action=remove_edges_by_attribute, edge_attribute=paired_concept_frequency, direction=below, threshold=0.0000001, remove_connected_nodes=t, qnode_key=n02)",  # remove drugs below COHD threshold
            #"overlay(action=compute_jaccard, start_node_key=n01, intermediate_node_key=n02, end_node_key=n03, virtual_relation_label=J2)",  # look at proteins that share many/any drugs in common with the phenotypes
            #"filter_kg(action=remove_edges_by_attribute, edge_attribute=jaccard_index, direction=below, threshold=.001, remove_connected_nodes=t, qnode_key=n03)",
            #"filter_kg(action=remove_edges_by_type, edge_type=J2, remove_connected_nodes=f)",
            #"filter_kg(action=remove_edges_by_type, edge_type=C1, remove_connected_nodes=f)",
            ##"overlay(action=compute_ngd)",
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 15:  # FIXME NOTE: this is our planned example 3 (so don't fix, it's just so it's highlighted in my IDE)
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=DOID:9406, key=n00)",  # hypopituitarism
            "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n01)",  # look for all drugs associated with this disease (29 total drugs)
            "add_qnode(categories=biolink:Protein, key=n02)",   # look for proteins associated with these diseases (240 total proteins)
            "add_qedge(subject=n00, object=n01, key=e00)",  # get connections
            "add_qedge(subject=n01, object=n02, key=e01)",  # get connections
            "expand(edge_id=[e00,e01])",  # expand the query graph
            "overlay(action=overlay_clinical_info, observed_expected_ratio=true, virtual_relation_label=C1, subject_qnode_key=n00, object_qnode_key=n01)",  # Look in COHD to find which drug are being used to treat this disease based on the log ratio of expected frequency of this drug being used to treat a disease, vs. the observed number of times it’s used to treat this disease
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=observed_expected_ratio, direction=below, threshold=3, remove_connected_nodes=t, qnode_key=n01)",   # concentrate only on those drugs that are more likely to be treating this disease than expected
            "filter_kg(action=remove_orphaned_nodes, node_category=biolink:Protein)",  # remove proteins that got disconnected as a result of this filter action
            "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n01, object_qnode_key=n02)",   # use normalized google distance to find how frequently the protein and the drug are mentioned in abstracts
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=normalized_google_distance, direction=above, threshold=0.85, remove_connected_nodes=t, qnode_key=n02)",   # remove proteins that are not frequently mentioned together in PubMed abstracts
            "resultify(ignore_edge_direction=true)",
            "return(message=true, store=true)"
        ]}}
    elif params.example_number == 1515:  # Exact duplicate of ARAX_Example3.ipynb
        query = {"operations": {"actions": [
            "add_qnode(ids=DOID:9406, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n01)",
            "add_qnode(categories=biolink:Protein, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01)",
            "expand(edge_id=[e00,e01])",
            "overlay(action=overlay_clinical_info, observed_expected_ratio=true, virtual_relation_label=C1, subject_qnode_key=n00, object_qnode_key=n01)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=observed_expected_ratio, direction=below, threshold=3, remove_connected_nodes=t, qnode_key=n01)",
            "filter_kg(action=remove_orphaned_nodes, node_category=biolink:Protein)",
            "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n01, object_qnode_key=n02)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=normalized_google_distance, direction=above, threshold=0.85, remove_connected_nodes=t, qnode_key=n02)",
            "resultify(ignore_edge_direction=true)",
            "return(message=true, store=true)"
        ]}}
    elif params.example_number == 16:  # To test COHD
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:8398, key=n00)",
            #"add_qnode(name=DOID:1227, key=n00)",
            "add_qnode(categories=biolink:PhenotypicFeature, key=n01)",
            "add_qedge(subject=n00, object=n01, type=has_phenotype, key=e00)",
            "expand(edge_key=e00)",
            "overlay(action=overlay_clinical_info, chi_square=true)",
            "resultify()",
            "return(message=true, store=true)"
        ]}}
    elif params.example_number == 17:  # Test resultify #FIXME: this returns a single result instead of a list (one for each disease/phenotype found)
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:731, key=n00, type=disease, is_set=false)",
            "add_qnode(categories=biolink:PhenotypicFeature, is_set=false, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00)",
            'resultify(ignore_edge_direction=true)',
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 18:  # test removing orphaned nodes
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:9406, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n01)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01, type=physically_interacts_with)",
            "expand(edge_id=[e00, e01])",
            "filter_kg(action=remove_edges_by_type, edge_type=physically_interacts_with, remove_connected_nodes=f)",
            "filter_kg(action=remove_orphaned_nodes, node_category=biolink:Protein)",
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 19:  # Let's see what happens if you ask for a node in KG2, but not in KG1 and try to expand
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=UMLS:C1452002, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00, type=interacts_with)",
            "expand(edge_key=e00)",
            "return(message=true, store=false)"
        ]}}  # returns response of "OK" with the info: QueryGraphReasoner found no results for this query graph
    elif params.example_number == 20:  # Now try with KG2 expander
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=UMLS:C1452002, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00, type=interacts_with)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "return(message=true, store=false)"
        ]}}  # returns response of "OK" with the info: QueryGraphReasoner found no results for this query graph
    elif params.example_number == 101:  # test of filter results code
        query = { "operations": { "actions": [
            "create_message",
            "add_qnode(name=DOID:14330, key=n00)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01, type=physically_interacts_with)",
            "expand(edge_id=[e00,e01])",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=jaccard_index, direction=below, threshold=.2, remove_connected_nodes=t, qnode_key=n02)",
            "filter_kg(action=remove_edges_by_property, edge_property=provided_by, property_value=Pharos)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_edge_attribute, edge_attribute=jaccard_index, direction=d, max_results=15)",
            #"filter_results(action=sort_by_edge_count, direction=a)",
            #"filter_results(action=limit_number_of_results, max_results=5)",
            "return(message=true, store=false)",
            ] } }
    elif params.example_number == 102:  # add pubmed id's
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:1227, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00)",
            "overlay(action=add_node_pmids, max_num=15)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_node_attribute, node_attribute=pubmed_ids, direction=a, max_results=20)",
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 103:  # add pubmed id's
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:1227, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00)",
            "overlay(action=add_node_pmids, max_num=15)",
            "filter_kg(action=remove_nodes_by_property, node_property=uri, property_value=https://www.ebi.ac.uk/chembl/compound/inspect/CHEMBL2111164)",
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 1212:  # dry run of example 2 with the machine learning model
        query = { "operations": { "actions": [
            "create_message",
            "add_qnode(ids=DOID:14330, key=n00)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01, type=physically_interacts_with)",
            "expand(edge_id=[e00,e01], kp=ARAX/KG1)",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=jaccard_index, direction=below, threshold=.2, remove_connected_nodes=t, qnode_key=n02)",
            "filter_kg(action=remove_edges_by_property, edge_property=provided_by, property_value=Pharos)",  # can be removed, but shows we can filter by Knowledge provider
            "overlay(action=predict_drug_treats_disease, subject_qnode_key=n02, object_qnode_key=n00, virtual_relation_label=P1)",  # overlay by probability that the drug treats the disease
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_edge_attribute, edge_attribute=probability_drug_treats, direction=descending, max_results=15)",  # filter by the probability that the drug treats the disease. cilnidipine prob=0.8976650309881645 which is the 9th highest (so top 10)
            "return(message=true, store=false)",
            ] } }
    elif params.example_number == 201:  # KG2 version of demo example 1 (acetaminophen)
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(key=n00, ids=CHEMBL.COMPOUND:CHEMBL112)",  # acetaminophen
            "add_qnode(key=n01, categories=biolink:Protein, is_set=true)",
            "add_qedge(key=e00, subject=n00, object=n01)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "return(message=true, store=false)",
        ]}}
    elif params.example_number == 202:  # KG2 version of demo example 2 (Parkinson's)
        query = { "operations": { "actions": [
            "create_message",
            "add_qnode(name=DOID:14330, key=n00)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01, type=molecularly_interacts_with)",  # for KG2
            #"add_qedge(subject=n01, object=n02, key=e01, type=physically_interacts_with)",  # for KG1
            "expand(edge_id=[e00,e01], kp=infores:rtx-kg2)",  # for KG2
            #"expand(edge_id=[e00,e01], kp=ARAX/KG1)",  # for KG1
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",  # seems to work just fine
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=jaccard_index, direction=below, threshold=.008, remove_connected_nodes=t, qnode_key=n02)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_edge_attribute, edge_attribute=jaccard_index, direction=descending, max_results=15)",
            "return(message=true, store=false)",
            ] } }
    elif params.example_number == 203:  # KG2 version of demo example 3 (but using idiopathic pulmonary fibrosis)
        query = { "operations": { "actions": [
            "create_message",
            #"add_qnode(key=n00, ids=DOID:0050156)",  # idiopathic pulmonary fibrosis
            "add_qnode(ids=DOID:9406, key=n00)",  # hypopituitarism, original demo example
            "add_qnode(key=n01, categories=biolink:ChemicalEntity, is_set=true)",
            "add_qnode(key=n02, categories=biolink:Protein)",
            "add_qedge(key=e00, subject=n00, object=n01)",
            "add_qedge(key=e01, subject=n01, object=n02)",
            "expand(edge_id=[e00,e01], kp=infores:rtx-kg2)",
            "overlay(action=overlay_clinical_info, observed_expected_ratio=true, virtual_relation_label=C1, subject_qnode_key=n00, object_qnode_key=n01)",
            "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n01, object_qnode_key=n02)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=observed_expected_ratio, direction=below, threshold=2, remove_connected_nodes=t, qnode_key=n01)",
            "filter_kg(action=remove_orphaned_nodes, node_category=biolink:Protein)",
            "return(message=true, store=false)",
            ] } }
    elif params.example_number == 2033:  # KG2 version of demo example 3 (but using idiopathic pulmonary fibrosis), with all decorations
        query = { "operations": { "actions": [
            "create_message",
            "add_qnode(key=n00, id=DOID:0050156)",  # idiopathic pulmonary fibrosis
            #"add_qnode(ids=DOID:9406, key=n00)",  # hypopituitarism, original demo example
            "add_qnode(key=n01, categories=biolink:ChemicalEntity, is_set=true)",
            "add_qnode(key=n02, categories=biolink:Protein)",
            "add_qedge(key=e00, subject=n00, object=n01)",
            "add_qedge(key=e01, subject=n01, object=n02)",
            "expand(edge_id=[e00,e01], kp=infores:rtx-kg2)",
            "overlay(action=overlay_clinical_info, observed_expected_ratio=true, virtual_relation_label=C1, subject_qnode_key=n00, object_qnode_key=n01)",
            "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n01, object_qnode_key=n02)",
            #"filter_kg(action=remove_edges_by_attribute, edge_attribute=observed_expected_ratio, direction=below, threshold=0, remove_connected_nodes=t, qnode_key=n01)",
            #"filter_kg(action=remove_orphaned_nodes, node_category=biolink:Protein)",
            "return(message=true, store=false)",
            ] } }
    elif params.example_number == 222:  # Simple BTE query
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(key=n00, ids=NCBIGene:1017)",  # CDK2
            "add_qnode(key=n01, categories=biolink:ChemicalEntity, is_set=True)",
            "add_qedge(key=e00, subject=n01, object=n00)",
            "expand(edge_key=e00, kp=infores:biothings-explorer)",
            "return(message=true, store=false)",
        ]}}
    elif params.example_number == 233:  # KG2 version of demo example 1 (acetaminophen)
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(key=n00, ids=CHEMBL.COMPOUND:CHEMBL112)",  # acetaminophen
            "add_qnode(key=n01, categories=biolink:Protein, is_set=true)",
            "add_qedge(key=e00, subject=n00, object=n01)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "filter_kg(action=remove_edges_by_property, edge_property=provided_by, property_value=https://pharos.nih.gov)",
            "return(message=true, store=false)",
        ]}}
    elif params.example_number == 300:  # KG2 version of demo example 1 (acetaminophen)
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:14330, key=n00)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01, type=physically_interacts_with)",
            "expand(edge_id=[e00,e01], kp=ARAX/KG1)",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_edge_type=J1)",
            "filter_kg(action=remove_edges_by_attribute_default, edge_attribute=jaccard_index, type=std, remove_connected_nodes=t, qnode_key=n02)",
            #"filter_kg(action=remove_edges_by_property, edge_property=provided_by, property_value=Pharos)",  # can be removed, but shows we can filter by Knowledge provider
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_edge_attribute, edge_attribute=jaccard_index, direction=descending, max_results=15)",
            "return(message=true, store=false)",
        ]}}
    elif params.example_number == 690:  # test issue 690
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:14330, key=n00)",
            "add_qnode(type=not_a_real_type, is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01, type=molecularly_interacts_with)",
            "expand(edge_id=[e00,e01], continue_if_no_results=true)",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 6231:  # chunyu testing #623, all nodes already in the KG and QG
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(key=n00, ids=CHEMBL.COMPOUND:CHEMBL521, categories=biolink:ChemicalEntity)",
            "add_qnode(key=n01, is_set=true, categories=biolink:Protein)",
            "add_qedge(key=e00, subject=n00, object=n01)",
            "add_qnode(key=n02, type=biological_process)",
            "add_qedge(key=e01, subject=n01, object=n02)",
            "expand(edge_id=[e00, e01], kp=ARAX/KG1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n01, virtual_relation_label=FET, object_qnode_key=n02, cutoff=0.05)",
            "resultify()",
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 6232:  # chunyu testing #623, this should return the 10 smallest FET p-values and only add the virtual edge with top 10 FET p-values
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(key=n00, ids=CHEMBL.COMPOUND:CHEMBL521, categories=biolink:ChemicalEntity)",
            "add_qnode(key=n01, is_set=true, categories=biolink:Protein)",
            "add_qedge(key=e00, subject=n00, object=n01)",
            "add_qnode(key=n02, type=biological_process)",
            "add_qedge(key=e01, subject=n01, object=n02)",
            "expand(edge_id=[e00, e01], kp=ARAX/KG1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n01, virtual_relation_label=FET, object_qnode_key=n02, top_n=10)",
            "resultify()",
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 6233:  # chunyu testing #623, this DSL tests the FET module based on (source id - involved_in - target id) and only decorate/add virtual edge with pvalue<0.05
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(key=n00, ids=CHEMBL.COMPOUND:CHEMBL521, categories=biolink:ChemicalEntity)",
            "add_qnode(key=n01, is_set=true, categories=biolink:Protein)",
            "add_qedge(key=e00, subject=n00, object=n01)",
            "add_qnode(key=n02, type=biological_process)",
            "add_qedge(key=e01, subject=n01, object=n02, type=involved_in)",
            "expand(edge_id=[e00, e01], kp=ARAX/KG1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n01, virtual_relation_label=FET, object_qnode_key=n02, rel_edge_key=e01, cutoff=0.05)",
            "resultify()",
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 6234:  # chunyu testing #623, nodes not in the KG and QG. This should throw an error initially. In the future we might want to add these nodes.
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(key=n00, ids=CHEMBL.COMPOUND:CHEMBL521, categories=biolink:ChemicalEntity)",
            "add_qnode(key=n01, categories=biolink:Protein)",
            "add_qedge(key=e00, subject=n00, object=n01)",
            "expand(edge_id=[e00], kp=ARAX/KG1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n01, virtual_relation_label=FET, object_qnode_key=n02, cutoff=0.05)",
            "resultify()",
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 6235:  # chunyu testing #623, this is a two-hop sample. First, find all edges between DOID:14330 and proteins and then filter out the proteins with connection having pvalue>0.001 to DOID:14330. Second, find all edges between proteins and chemical_substances and then filter out the chemical_substances with connection having pvalue>0.005 to proteins
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=DOID:14330, key=n00, type=disease)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00, kp=ARAX/KG1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n00, object_qnode_key=n01, virtual_relation_label=FET1)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.001, remove_connected_nodes=t, qnode_key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n01, object=n02, key=e01, type=physically_interacts_with)",
            "expand(edge_key=e01, kp=ARAX/KG1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n01, object_qnode_key=n02, virtual_relation_label=FET2)",
            "resultify()",
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 6236:  # chunyu testing #623, this is a three-hop sample: DOID:14330 - protein - (physically_interacts_with) - chemical_substance - phenotypic_feature
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=DOID:14330, key=n00, type=disease)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00, kp=ARAX/KG1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n00, object_qnode_key=n01, virtual_relation_label=FET1)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.001, remove_connected_nodes=t, qnode_key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n02)",
            "add_qedge(subject=n01, object=n02, key=e01, type=physically_interacts_with)",
            "expand(edge_key=e01, kp=ARAX/KG1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n01, object_qnode_key=n02, virtual_relation_label=FET2)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.001, remove_connected_nodes=t, qnode_key=n02)",
            "add_qnode(categories=biolink:PhenotypicFeature, key=n03)",
            "add_qedge(subject=n02, object=n03, key=e02)",
            "expand(edge_key=e02, kp=ARAX/KG1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n02, object_qnode_key=n03, virtual_relation_label=FET3)",
            "resultify()",
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 6237:  # chunyu testing #623, this is a four-hop sample: CHEMBL521 - protein - biological_process - protein - disease
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(key=n00, ids=CHEMBL.COMPOUND:CHEMBL521, categories=biolink:ChemicalEntity)",
            "add_qnode(key=n01, is_set=true, categories=biolink:Protein)",
            "add_qedge(key=e00, subject=n00, object=n01)",
            "expand(edge_key=e00, kp=ARAX/KG1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n00, object_qnode_key=n01, virtual_relation_label=FET1)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.01, remove_connected_nodes=t, qnode_key=n01)",
            "add_qnode(type=biological_process, is_set=true, key=n02)",
            "add_qedge(subject=n01, object=n02, key=e01)",
            "expand(edge_key=e01, kp=ARAX/KG1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n01, object_qnode_key=n02, virtual_relation_label=FET2)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.01, remove_connected_nodes=t, qnode_key=n02)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n03)",
            "add_qedge(subject=n02, object=n03, key=e02)",
            "expand(edge_key=e02, kp=ARAX/KG1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n02, object_qnode_key=n03, virtual_relation_label=FET3)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.01, remove_connected_nodes=t, qnode_key=n03)",
            "add_qnode(type=disease, key=n04)",
            "add_qedge(subject=n03, object=n04, key=e03)",
            "expand(edge_key=e03, kp=ARAX/KG1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n03, object_qnode_key=n04, virtual_relation_label=FET4)",
            "resultify()",
            "return(message=true, store=false)"
        ]}}
    elif params.example_number == 7680:  # issue 768 test all but jaccard, uncomment any one you want to test
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=DOID:1588, key=n0)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
            "add_qedge(subject=n0, object=n1, key=e0)",
            "expand(edge_key=e0)",
            #"overlay(action=predict_drug_treats_disease)",
            #"overlay(action=predict_drug_treats_disease, subject_qnode_id=n1, object_qnode_key=n0, virtual_relation_label=P1)",
            #"overlay(action=overlay_clinical_info,paired_concept_frequency=true)",
            #"overlay(action=overlay_clinical_info,observed_expected_ratio=true)",
            #"overlay(action=overlay_clinical_info,chi_square=true)",
            #"overlay(action=overlay_clinical_info,paired_concept_frequency=true, subject_qnode_key=n0, object_qnode_key=n1, virtual_relation_label=CP1)",
            #"overlay(action=overlay_clinical_info,observed_expected_ratio=true, subject_qnode_key=n0, object_qnode_key=n1, virtual_relation_label=OE1)",
            #"overlay(action=overlay_clinical_info,chi_square=true, subject_qnode_key=n0, object_qnode_key=n1, virtual_relation_label=C1)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n0, object_qnode_key=n1, virtual_relation_label=FET)",
            "resultify()",
            "filter_results(action=limit_number_of_results, max_results=15)",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 7681:  # issue 768 with jaccard
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=DOID:14330, key=n00)",  # parkinsons
            "add_qnode(categories=biolink:Protein, is_set=True, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, is_set=False, key=n02)",
            "add_qedge(subject=n01, object=n00, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01)",
            "expand(edge_id=[e00,e01])",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
            "resultify()",
            "filter_results(action=limit_number_of_results, max_results=15)",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 7200:  # issue 720, example 2
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=DOID:14330, key=n00)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01, type=physically_interacts_with)",
            "expand(edge_id=[e00,e01], kp=ARAX/KG1)",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
            #"filter_kg(action=remove_edges_by_attribute, edge_attribute=jaccard_index, direction=below, threshold=.2, remove_connected_nodes=t, qnode_key=n02)",
            #"filter_kg(action=remove_edges_by_property, edge_property=provided_by, property_value=Pharos)",
            #"overlay(action=predict_drug_treats_disease, subject_qnode_key=n02, object_qnode_key=n00, virtual_relation_label=P1)",
            "resultify(ignore_edge_direction=true, debug=true)",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 885:
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:11830, key=n00)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01, type=molecularly_interacts_with)",
            "expand(edge_id=[e00,e01], kp=infores:rtx-kg2)",
            # overlay a bunch of clinical info
            "overlay(action=overlay_clinical_info, paired_concept_frequency=true, subject_qnode_key=n00, object_qnode_key=n02, virtual_relation_label=C1)",
            "overlay(action=overlay_clinical_info, observed_expected_ratio=true, subject_qnode_key=n00, object_qnode_key=n02, virtual_relation_label=C2)",
            "overlay(action=overlay_clinical_info, chi_square=true, subject_qnode_key=n00, object_qnode_key=n02, virtual_relation_label=C3)",
            # return results
            "resultify(ignore_edge_direction=true)",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 887:
        query = {"operations": {"actions": [
            "add_qnode(name=DOID:9406, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n01)",
            "add_qnode(categories=biolink:Protein, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01)",
            "expand(edge_id=[e00,e01])",
            "overlay(action=overlay_clinical_info, observed_expected_ratio=true, virtual_relation_label=C1, subject_qnode_key=n00, object_qnode_key=n01)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=observed_expected_ratio, direction=below, threshold=3, remove_connected_nodes=t, qnode_key=n01)",
            "filter_kg(action=remove_orphaned_nodes, node_category=biolink:Protein)",
            "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n01, object_qnode_key=n02)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=normalized_google_distance, direction=above, threshold=0.85, remove_connected_nodes=t, qnode_key=n02)",
            "resultify(ignore_edge_direction=true)",
            "return(message=true, store=true)"
        ]}}
    elif params.example_number == 892:  # drug disease prediction with BTE
        query = {"operations": {"actions": [
            "add_qnode(ids=DOID:11830, type=disease, key=n00)",
            "add_qnode(type=gene, ids=[UniProtKB:P39060, UniProtKB:O43829, UniProtKB:P20849], is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01)",
            "expand(kp=infores:biothings-explorer)",
            "overlay(action=predict_drug_treats_disease, subject_qnode_key=n02, object_qnode_key=n00, virtual_relation_label=P1)",
            "resultify(ignore_edge_direction=true)",
            "return(message=true, store=true)"
        ]}}
    elif params.example_number == 8922:  # drug disease prediction with BTE and KG2
        query = {"operations": {"actions": [
            "add_qnode(ids=DOID:11830, key=n0, type=disease)",
            "add_qnode(categories=biolink:ChemicalEntity, ids=n1)",
            "add_qedge(subject=n0, object=n1, ids=e1)",
            "expand(edge_id=e1, kp=infores:rtx-kg2)",
            "expand(edge_id=e1, kp=infores:biothings-explorer)",
            #"overlay(action=overlay_clinical_info, paired_concept_frequency=true)",
            #"overlay(action=overlay_clinical_info, observed_expected_ratio=true)",
            #"overlay(action=overlay_clinical_info, chi_square=true)",
            "overlay(action=predict_drug_treats_disease)",
            #"overlay(action=compute_ngd)",infores:biothings-explorer
            "resultify(ignore_edge_direction=true)",
            #"filter_results(action=limit_number_of_results, max_results=50)",
            "return(message=true, store=true)"
        ]}}
    elif params.example_number == 8671:  # test_one_hop_kitchen_sink_BTE_1
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=DOID:11830, key=n0, type=disease)",
            "add_qnode(categories=biolink:ChemicalEntity, ids=n1)",
            "add_qedge(subject=n0, object=n1, ids=e1)",
            # "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "expand(edge_id=e1, kp=infores:biothings-explorer)",
            "overlay(action=overlay_clinical_info, paired_concept_frequency=true)",
            "overlay(action=overlay_clinical_info, observed_expected_ratio=true)",
            "overlay(action=overlay_clinical_info, chi_square=true)",
            "overlay(action=predict_drug_treats_disease)",
            "overlay(action=compute_ngd)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=limit_number_of_results, max_results=50)",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 8672:  # test_one_hop_based_on_types_1
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:11830, key=n00, type=disease)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "expand(edge_key=e00, kp=infores:biothings-explorer)",
            "overlay(action=overlay_clinical_info, observed_expected_ratio=true)",
            "overlay(action=predict_drug_treats_disease)",
            "filter_kg(action=remove_edges_by_attribute, edge_attribute=probability_treats, direction=below, threshold=0.75, remove_connected_nodes=true, qnode_key=n01)",
            "overlay(action=compute_ngd)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=limit_number_of_results, max_results=50)",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 8673:  # test_one_hop_based_on_types_1
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=MONDO:0001475, key=n00, type=disease)",
            "add_qnode(categories=biolink:Protein, key=n01, is_set=true)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01, type=molecularly_interacts_with)",
            "expand(edge_id=[e00,e01], kp=infores:rtx-kg2, continue_if_no_results=true)",
            #- expand(edge_id=[e00,e01], kp=infores:biothings-explorer, continue_if_no_results=true)",
            "expand(edge_key=e00, kp=infores:biothings-explorer, continue_if_no_results=true)",
            #- expand(edge_key=e00, kp=infores:genetics-data-provider, continue_if_no_results=true)",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
            "overlay(action=predict_drug_treats_disease, subject_qnode_key=n02, object_qnode_key=n00, virtual_relation_label=P1)",
            "overlay(action=overlay_clinical_info, chi_square=true, virtual_relation_label=C1, subject_qnode_key=n00, object_qnode_key=n02)",
            #"overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n00, object_qnode_key=n01)",
            #"overlay(action=compute_ngd, virtual_relation_label=N2, subject_qnode_key=n00, object_qnode_key=n02)",
            #"overlay(action=compute_ngd, virtual_relation_label=N3, subject_qnode_key=n01, object_qnode_key=n02)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=limit_number_of_results, max_results=100)",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 9999:
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=acetaminophen, key=n0)",
            "add_qnode(categories=biolink:Protein, key=n1)",
            "add_qedge(subject=n0, object=n1, key=e0)",
            "expand(edge_key=e0)",
            "resultify()",
            "filter_results(action=limit_number_of_results, max_results=100)",
            "return(message=true, store=json)",
        ]}}
    elif params.example_number == 1492:
        query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=MONDO:0005301, key=n0)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
            "add_qedge(subject=n0, object=n1, key=e0, predicates=biolink:related_to)",
            "expand(kp=infores:biothings-multiomics-clinical-risk, edge_key=e0)",
            "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n0, object_qnode_key=n1)",
            "resultify()",
            "return(message=true, store=true)",
        ]}}
    elif params.example_number == 1848:
        query = {"operations": {"actions": ["add_qnode(key=n0, ids=[MONDO:0009061], is_set=false)",
            "add_qnode(key=n1, is_set=false, categories=[biolink:ChemicalEntity])",
            "add_qedge(key=e0, subject=n0, object=n1, predicates=[biolink:has_real_world_evidence_of_association_with])",
            "expand(kp=infores:cohd)",
            "overlay(action=compute_ngd,default_value=inf,virtual_relation_label=N1,subject_qnode_key=n0,object_qnode_key=n1)",
            "scoreless_resultify(ignore_edge_direction=true)",
            "rank_results()",
            "filter_results(action=limit_number_of_results,max_results=3,prune_kg=true)",
            "add_qnode(key=n2, is_set=false, categories=[biolink:Gene,biolink:Protein])",
            "add_qedge(key=e1, subject=n1, object=n2, predicates=[biolink:increases_activity_of])",
            "add_qnode(key=n3, is_set=false, categories=[biolink:ChemicalEntity])",
            "add_qedge(key=e2, subject=n3, object=n2, predicates=[biolink:increases_activity_of])",
            "add_qedge(key=e3, subject=n1, object=n2, predicates=[biolink:decreases_activity_of], option_group_id=decr)",
            "add_qedge(key=e4, subject=n3, object=n2, predicates=[biolink:decreases_activity_of], option_group_id=decr)",
            "expand(kp=infores:rtx-kg2)",
            "scoreless_resultify(ignore_edge_direction=true)",
            "rank_results()"
        ]}}
    else:
        eprint(f"Invalid test number {params.example_number}. Try 1 through 17")
        return


    #### Execute the query
    araxq.query(query)
    response = araxq.response

    #### If the result was an error, just end here
    #if response.status != 'OK':
    #    print(response.show(level=ARAXResponse.DEBUG))
    #    return response

    #### Retrieve the TRAPI Response (envelope) and TRAPI Message from the result
    envelope = response.envelope
    message = envelope.message
    envelope.status = response.error_code
    envelope.description = response.message


    #### Print out the logging stream
    print(response.show(level=ARAXResponse.DEBUG))

    #### Print out the message that came back
    print(json.dumps(ast.literal_eval(repr(envelope)), sort_keys=True, indent=2))

    #### Other stuff that could be dumped
    #print(json.dumps(message.to_dict(),sort_keys=True,indent=2))
    #print(json.dumps(ast.literal_eval(repr(message.id)), sort_keys=True, indent=2))
    #print(json.dumps(ast.literal_eval(repr(message.knowledge_graph.edges)), sort_keys=True, indent=2))
    #print(json.dumps(ast.literal_eval(repr(message.query_graph)), sort_keys=True, indent=2))
    #print(json.dumps(ast.literal_eval(repr(message.knowledge_graph.nodes)), sort_keys=True, indent=2))
    #print(response.show(level=ARAXResponse.DEBUG))

    print(f"Number of results: {len(message.results)}")

    #print(f"Drugs names in the KG: {[x.name for x in message.knowledge_graph.nodes if 'chemical_substance' in x.type or 'drug' in x.type]}")

    #print(f"Essence names in the answers: {[x.essence for x in message.results]}")
    print("Results:")
    for result in message.results:
        confidence = result.confidence
        if confidence is None:
            confidence = 0.0
        print("  -" + '{:6.3f}'.format(confidence) + f"\t{result.essence}")

    # print the response id at the bottom for convenience too:
    print(f"Returned response id: {envelope.id}")

if __name__ == "__main__":
    main()


# code/ARAX/ARAXQuery/ARAX_query_graph_interpreter.py

#!/bin/env python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast
import re
import yaml
from datetime import datetime

from ARAX_response import ARAXResponse
from query_graph_info import QueryGraphInfo
from knowledge_graph_info import KnowledgeGraphInfo
from ARAX_messenger import ARAXMessenger

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../")
from RTXConfiguration import RTXConfiguration

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../UI/OpenAPI/python-flask-server/")
from openapi_server.models.message import Message
from openapi_server.models.knowledge_graph import KnowledgeGraph
from openapi_server.models.query_graph import QueryGraph
from openapi_server.models.q_node import QNode
from openapi_server.models.q_edge import QEdge


class ARAXQueryGraphInterpreter:

    #### Constructor
    def __init__(self):
        self.response = None
        self.message = None
        self.parameters = None

        self.query_graph_templates = None
        self.query_graph_tree = None

        self.read_query_graph_templates()


    # #### Create a fresh Message object and fill with defaults
    def translate_to_araxi(self, response, describe=False):
        """
        Translate an input query_graph into ARAXi
        :return: ARAXResponse object with execution information and the DSL command set
        :rtype: ARAXResponse
        """

        #### Extract the message from the response
        message = response.envelope.message
        debug = False

        #### Ensure that query_graph_templates is ready
        if self.query_graph_templates is None:
            response.error("QueryGraph templates cannot be read from reference file", error_code="QueryGraphInterpreterMissingTemplates")
            return response

        query_graph_info = QueryGraphInfo()

        result = query_graph_info.assess(message)
        response.merge(result)
        if result.status != 'OK':
            #print(response.show(level=ARAXResponse.DEBUG))
            return response

        query_graph_template = query_graph_info.query_graph_templates['detailed']
        #print(json.dumps(query_graph_template,sort_keys=True,indent=2))

        # Check the number of nodes since the tree is based on the number of nodes
        n_nodes = query_graph_template['n_nodes']
        if n_nodes not in self.query_graph_tree['n_nodes']:
            response.error("QueryGraphInterpreter finds more nodes than supported in this QueryGraph", error_code="QueryGraphInterpreterUnsupportedGraph")
            return response

        # Set up a list of tree pointers with both a pointer to the next dict as well as the running score total
        # There are potentially multiple matches to track especially since we permit less specific matches
        tree_pointers = [ { 'pointer': self.query_graph_tree['n_nodes'][n_nodes], 'score': 0 } ]

        # Now look over each component looking for matches
        possible_next_steps = []
        for component in query_graph_template['components']:
            if debug: print(f"- Component is {component}")
            possible_next_steps = []

            #### If the component is a node, then score it
            if component['component_type'] == 'node':

                # Go through the list of possible things it could be and those or lesser possible next steps
                if component['has_ids'] and component['has_categories'] and component['categories_value']:
                    possible_next_steps.append( { 'content': f"ids,categories={component['categories_value']}", 'score': 10000 } )
                    possible_next_steps.append( { 'content': 'ids', 'score': 1000 } )
                    possible_next_steps.append( { 'content': '', 'score': 0 } )

                elif component['has_ids']:
                    possible_next_steps.append( { 'content': 'ids', 'score': 1000 } )
                    possible_next_steps.append( { 'content': '', 'score': 0 } )

                elif component['has_categories'] and component['categories_value']:
                    possible_next_steps.append( { 'content': f"categories={component['categories_value']}", 'score': 100 } )
                    possible_next_steps.append( { 'content': 'categories', 'score': 10 } )
                    possible_next_steps.append( { 'content': '', 'score': 0 } )

                elif component['has_categories']:
                    possible_next_steps.append( { 'content': 'categories', 'score': 10 } )
                    possible_next_steps.append( { 'content': '', 'score': 0 } )

                else:
                    possible_next_steps.append( { 'content': '', 'score': 0 } )

            # Else it's an edge. Don't do anything with those currently
            else:
                # Go through the list of possible things it could be and those or lesser possible next steps
                if component['has_predicates'] and component['predicates_value']:
                    possible_next_steps.append( { 'content': f"predicates={component['predicates_value']}", 'score': 90 } )
                    possible_next_steps.append( { 'content': 'predicates', 'score': 10 } )
                    possible_next_steps.append( { 'content': '', 'score': 0 } )

                elif component['has_predicates']:
                    possible_next_steps.append( { 'content': 'predicates', 'score': 10 } )
                    possible_next_steps.append( { 'content': '', 'score': 0 } )

                else:
                    possible_next_steps.append( { 'content': '', 'score': 0 } )

            # For each of the current tree pointers
            new_tree_pointers = []
            for tree_pointer in tree_pointers:
                if debug:
                    #print(f"    - pointer={tree_pointer}")
                    #print(f"    - pointer...")
                    #for tp_key,tp_pointer in tree_pointer['pointer'].items():
                    #    print(f"        - {tp_key} = {tp_pointer}")
                    pass

                # Consider each of the new possibilities
                for possible_next_step in possible_next_steps:
                    component_string = f"{component['component_id']}({possible_next_step['content']})"
                    if debug: print(f"    - component_string={component_string}")

                    # If this component is a possible next step in the tree, then add the next step to new_tree_pointers
                    if component_string in tree_pointer['pointer']:
                        if debug: print(f"      - Found this component with score {possible_next_step['score']}")
                        new_tree_pointers.append( { 'pointer': tree_pointer['pointer'][component_string], 'score': tree_pointer['score'] + possible_next_step['score'] })
                        #tree_pointer = tree_pointer[component_string]

            # When we're done, reset the surviving tree pointers
            tree_pointers = new_tree_pointers

        # Now determine the best scoring match and assign that one
        query_graph_template_name = '??'
        best_score = -1
        for tree_pointer in tree_pointers:
            if 'name' in tree_pointer['pointer']:
                if debug: print(f"==> Found template is {tree_pointer['pointer']['name']} with score {tree_pointer['score']}")
                if tree_pointer['score'] > best_score:
                    query_graph_template_name = tree_pointer['pointer']['name']
                    best_score = tree_pointer['score']


        # If the final best template name is a real one in templates, then get the ARAXI for it
        if query_graph_template_name in self.query_graph_templates['templates']:
            araxi_commands = self.query_graph_templates['templates'][query_graph_template_name]['DSL']

            # Need to remap the theoretical node and edge ids into the actual ones
            # Do it in two passes in case there are overlaps in the names. Issue #1457
            new_araxi_commands = []
            tmp_araxi_commands = []
            for command in araxi_commands:
                node_index = 0
                new_command = command
                for node in query_graph_info.node_order:
                    template_id = f"n{node_index:02}"
                    temp_template_id = f"zzxxyqn{node_index:02}"
                    new_command = re.sub(template_id,temp_template_id,new_command)
                    node_index += 1

                edge_index = 0
                for edge in query_graph_info.edge_order:
                    template_id = f"e{edge_index:02}"
                    temp_template_id = f"zzxxyqe{node_index:02}"
                    new_command = re.sub(template_id,temp_template_id,new_command)
                    edge_index += 1

                tmp_araxi_commands.append(new_command)

            #### Second pass remapping the temporary names
            for command in tmp_araxi_commands:
                node_index = 0
                new_command = command
                for node in query_graph_info.node_order:
                    template_id = f"zzxxyqn{node_index:02}"
                    new_command = re.sub(template_id,node['key'],new_command)
                    node_index += 1

                edge_index = 0
                for edge in query_graph_info.edge_order:
                    template_id = f"zzxxyqe{edge_index:02}"
                    new_command = re.sub(template_id,edge['key'],new_command)
                    edge_index += 1

                new_araxi_commands.append(new_command)

            # TODO: Create the restated_question from the template
            response.data['araxi_commands'] = new_araxi_commands
            return response

        # response.error("QueryGraphInterpreter cannot interpret this QueryGraph", error_code="QueryGraphInterpreterUnsupportedGraph")

        # If we got here, then no templates matches, so try to assign a bland program and hope for the best
        response.warning("QueryGraphInterpreter cannot match this QueryGraph to a known template, just running a default template")
        response.data['araxi_commands'] = [
            'expand()',
            'resultify()',
            'filter_results(action=limit_number_of_results, max_results=500)'
        ]

        return response


    # #### Read the YAML file containing the current QueryGraph templates
    def read_query_graph_templates(self):
        """
        Read the YAML file containing the current QueryGraph templates
        :rcategory: None
        """

        # The template file is stored right next to this code
        template_file = os.path.dirname(os.path.abspath(__file__))+"/ARAX_query_graph_interpreter_templates.yaml"

        # If the template file is not found, record an error and return
        if not os.path.exists(template_file):
            self.response.error("QueryGraphInterpreter templates file is missing", error_code="QueryGraphInterpreterTemplateMissing")
            return self.response

        # Open the file and try to load it
        with open(template_file, 'r') as stream:
            try:
                self.query_graph_templates = yaml.safe_load(stream)
            except yaml.YAMLError as exc:
                self.response.error(f"Error parsing YAML file template_file: {exc}", error_code="CannotParseQueryGraphInterpreterTemplate")
                return self.response

        # Make sure the version is as expected
        if 'ARAX_QG_DSL_mapping' not in self.query_graph_templates:
            self.response.error(f"Missing version number in QueryGraphInterpreter templates file {template_file}", error_code="MissingQueryGraphInterpreterTemplateFileVersion")
            self.query_graph_templates = None
            return self.response
        if self.query_graph_templates['ARAX_QG_DSL_mapping'] != 0.2:
            self.response.error(f"Incorrect version number in QueryGraphInterpreter templates file {template_file}", error_code="BadQueryGraphInterpreterTemplateFileVersion")
            self.query_graph_templates = None
            return self.response

        # We will create dict lookup table of all the template string [e.g. 'n00(ids)-e00()-n01(categories)' -> template_name]
        self.query_graph_templates['template_strings'] = {}

        # We will also create dict tree of all templates organized by the number of nodes and then by each component
        self.query_graph_tree = { 'n_nodes': {} }

        # Loop over all the templates in the YAML file
        for template_name,template in self.query_graph_templates['templates'].items():

            # Initialize an empty string to build the template in 
            template_string = ''
            i = 0

            # Determine the number of components and nodes in this template and start building a tree
            n_components = len(template['template'])
            n_nodes = int( ( n_components + 1 ) / 2 )
            if n_nodes not in self.query_graph_tree['n_nodes']:
                self.query_graph_tree['n_nodes'][n_nodes] = {}

            # previous_dict will contain the last place we left off in the tree
            previous_dict = self.query_graph_tree['n_nodes'][n_nodes]

            # Loop over each component building the tree
            for component in template['template']:

                # If this branch of the tree doesn't exist yet, add it
                if component not in previous_dict:
                    previous_dict[component] = {}

                # This location will be the next starting point
                previous_dict = previous_dict[component]

                # Append to the end of the template string
                if i > 0:
                    template_string += '-'
                template_string += component

                # If this is the last component, then attach the name of this template at the end
                i += 1
                if i == n_components:
                    previous_dict['name'] = template_name

            # Store the created template string and name
            self.query_graph_templates['template_strings'][template_string] = template_name

            # Cruft
                #match = re.match(r'([a-z]\d\d)\((.*)\)',component)
#                    parameters = match.group(2)
#                    if match.group(1).startswith('n'):
#                        print(f"  This is a node with parameters '{parameters}'")
#                        if parameters > '':
#                            parts = parameters.split(',')
#                            for part in parts:



##########################################################################################

def QGI_test1():

    #### Some qnode examples
    test_query_graphs = [
        [ { 'id': 'n10', 'curie': 'DOID:9281', 'category': 'disease'}, { 'id': 'n11', 'category': 'chemical_substance'}, { 'id': 'e10', 'source_id': 'n10', 'target_id': 'n11', 'category': 'treats'} ],
        [ { 'id': 'n10', 'curie': 'DOID:9281'}, { 'id': 'n11', 'category': 'protein'}, { 'id': 'e10', 'source_id': 'n10', 'target_id': 'n11'} ],
        [ { 'id': 'n10', 'curie': 'DOID:9281'}, { 'id': 'n11', 'category': 'protein'}, { 'id': 'n12', 'category': 'chemical_substance'},
            { 'id': 'e10', 'source_id': 'n10', 'target_id': 'n11'}, { 'id': 'e11', 'source_id': 'n11', 'target_id': 'n12'} ],
        [ { 'id': 'n10', 'curie': 'DOID:9281'}, { 'id': 'n11', 'category': 'chemical_substance'}, { 'id': 'e10', 'source_id': 'n10', 'target_id': 'n11'} ],
        [ { 'id': 'n10', 'curie': 'DOID:9281', 'category': 'disease'}, { 'id': 'n11', 'category': 'chemical_substance'}, { 'id': 'e10', 'source_id': 'n10', 'target_id': 'n11'} ],
    ]

    #interpreter = ARAXQueryGraphInterpreter()
    #print(json.dumps(interpreter.query_graph_tree,sort_keys=True,indent=2))
    #return

    for test_query_graph in test_query_graphs:

        #### Create a response object for each test
        response = ARAXResponse()

        #### Create a template Message
        messenger = ARAXMessenger()
        messenger.create_envelope(response)
        message = response.envelope.message

        for parameters in test_query_graph:
            if 'n' in parameters['id']:
                messenger.add_qnode(response, parameters)
                if response.status != 'OK':
                    print(response.show(level=ARAXResponse.DEBUG))
                    return response
            elif 'e' in parameters['id']:
                #print(f"++ Adding qedge with {parameters}")
                messenger.add_qedge(response, parameters)
                if response.status != 'OK':
                    print(response.show(level=ARAXResponse.DEBUG))
                    return response
            else:
                response.error(f"Unrecognized component {parameters['id']}")
                return response

        interpreter = ARAXQueryGraphInterpreter()
        interpreter.translate_to_araxi(response)
        if response.status != 'OK':
            print(response.show(level=ARAXResponse.DEBUG))
            return response

        araxi_commands = response.data['araxi_commands']
        for cmd in araxi_commands:
            print(f"  - {cmd}")

        #### Show the final result
        #print('-------------------------')
        #print(response.show(level=ARAXResponse.DEBUG))
        #print(json.dumps(message.to_dict(),sort_keys=True,indent=2))
        #sys.exit(1)


##########################################################################################

def QGI_test2():

    #### Set example query_graph
    # TRAPI 0.9.2
    input_query_graph = { "message": { "query_graph": { "nodes": [ { "id": "n1", "category": "chemical_substance" }, { "id": "n2", "curie": "UMLS:C0002395" } ], "edges": [ { "id": "e1", "predicate": "clinically_tested_approved_unknown_phase", "source_id": "n1", "target_id": "n2" } ] } } }
    # TRAPI 1.0.0
    input_query_graph = { "message": { "query_graph": { 
        "nodes": { "n1": { "category": "biolink:ChemicalEntity" }, "n2": { "id": "UMLS:C0002395" } },
        "edges": { "e1": { "predicate": "clinically_tested_approved_unknown_phase", "subject": "n1", "object": "n2" } }
        } } }
    # TRAPI 1.1.0
    input_query_graph = { "message": { "query_graph": { 
        "nodes": { "n1": { "categories": [ "biolink:ChemicalEntity" ] }, "n2": { "ids": [ "UMLS:C0002395" ] } },
        "edges": { "e1": { "predicates": [ "biolink:clinically_tested_approved_unknown_phase" ], "subject": "n1", "object": "n2" } }
        } } }

    #### Create a template Message
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    message = ARAXMessenger().from_dict(input_query_graph['message'])
    response.envelope.message.query_graph = message.query_graph

    interpreter = ARAXQueryGraphInterpreter()
    interpreter.translate_to_araxi(response)
    if response.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response

    araxi_commands = response.data['araxi_commands']
    for cmd in araxi_commands:
        print(f"  - {cmd}")

    #### Show the final result
    print('-------------------------')
    print(response.show(level=ARAXResponse.DEBUG))
    print(json.dumps(message.to_dict(),sort_keys=True,indent=2))


##########################################################################################

def QGI_test3():

    input_query_graph = { "message": { "query_graph": 
        {
        "nodes": {
            "n00": {
            "ids": [ "MONDO:0002715" ]
            },
            "n01": {
            "categories": [ "biolink:ChemicalEntity" ]
            },
            "n02": {
            "categories": [ "biolink:Gene" ]
            }
        },
        "edges": {
            "e00": {
            "predicates": [ "biolink:correlated_with" ],
            "subject": "n00",
            "object": "n01"
            },
            "e01": {
            "predicates": [ "biolink:related_to" ],
            "subject": "n01",
            "object": "n02"
            }
        }
        }
    } }

    #### Create a template Message
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    message = ARAXMessenger().from_dict(input_query_graph['message'])
    response.envelope.message.query_graph = message.query_graph

    interpreter = ARAXQueryGraphInterpreter()
    interpreter.translate_to_araxi(response)
    if response.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response

    araxi_commands = response.data['araxi_commands']
    for cmd in araxi_commands:
        print(f"  - {cmd}")

    #### Show the final result
    print('-------------------------')
    print(response.show(level=ARAXResponse.DEBUG))
    print(json.dumps(message.to_dict(),sort_keys=True,indent=2))
    #sys.exit(1)



##########################################################################################

def QGI_test4():

    input_query_graph = { "message": { "query_graph": 
            {
            "nodes": {
                "n00": {
                "categories": [
                    "biolink:Gene"
                ],
                "is_set": False
                },
                "n01": {
                "ids": [
                    "MONDO:0018177"
                ],
                "categories": [
                    "biolink:Disease"
                ],
                "is_set": False
                }
            },
            "edges": {
                "e00": {
                "subject": "n00",
                "object": "n01",
                "exclude": False
                }
            }
            }
    } }

    #### Create a template Message
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    message = ARAXMessenger().from_dict(input_query_graph['message'])
    response.envelope.message.query_graph = message.query_graph

    interpreter = ARAXQueryGraphInterpreter()
    interpreter.translate_to_araxi(response)
    if response.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response

    araxi_commands = response.data['araxi_commands']
    for cmd in araxi_commands:
        print(f"  - {cmd}")

    #### Show the final result
    #print('-------------------------')
    #print(response.show(level=ARAXResponse.DEBUG))
    #print(json.dumps(message.to_dict(),sort_keys=True,indent=2))
    #sys.exit(1)


def QGI_test5():
    # This is to test forked/non-linear queries (currently not working properly)
    input_query_graph = {
    "message": {
        "query_graph": {
            "nodes": {
                "n0": {
                    "categories": ["biolink:Gene"]
                },
                "n1": {
                    "ids": ["CHEBI:45783"],
                    "categories": ["biolink:ChemicalEntity"]
                },
                "n2": {
                    "ids": ["MONDO:0005301"],
                    "categories": ["biolink:Disease"]
                },
                "n3": {
                    "categories": ["biolink:ChemicalEntity"]
                }
            },
            "edges": {
                "e01": {
                    "subject": "n0",
                    "object": "n1",
                    "predicates": ["biolink:related_to"]
                },
                "e02": {
                    "subject": "n0",
                    "object": "n2",
                    "predicates": ["biolink:related_to"]
                },
                "e03": {
                    "subject": "n0",
                    "object": "n3",
                    "predicates": ["biolink:related_to"]
                }
            }
        }
    }
    }


    #### Create a template Message
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    message = ARAXMessenger().from_dict(input_query_graph['message'])
    response.envelope.message.query_graph = message.query_graph

    interpreter = ARAXQueryGraphInterpreter()
    interpreter.translate_to_araxi(response)
    if response.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response

    araxi_commands = response.data['araxi_commands']
    for cmd in araxi_commands:
        print(f"  - {cmd}")

    #### Show the final result
    #print('-------------------------')
    #print(response.show(level=ARAXResponse.DEBUG))
    #print(json.dumps(message.to_dict(),sort_keys=True,indent=2))
    #sys.exit(1)

def QGI_test6():
    # This is to test a three hop query with ends pinned (should result in FET ARAXi commands), and actually run the query
    input_query_graph = {
            "message": {
                "query_graph": {
          "edges": {
            "e00": {
              "object": "n01",
              "subject": "n00"
            },
            "e01": {
              "object": "n02",
              "subject": "n01"
            },
            "e02": {
              "object": "n03",
              "subject": "n02"
            }
          },
          "nodes": {
            "n00": {
              "categories": [
                "biolink:ChemicalEntity"
              ],
              "ids": [
                "DRUGBANK:DB00150"
              ]
            },
            "n01": {
              "categories": [
                "biolink:Protein"
              ]
            },
            "n02": {
              "categories": [
                "biolink:MolecularActivity"
              ]
            },
            "n03": {
              "categories": [
                "biolink:ChemicalEntity"
              ],
              "ids": [
                "KEGG.COMPOUND:C02700"
              ]
            }
          }
        }

    }
    }


    #### Create a template Message
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    message = ARAXMessenger().from_dict(input_query_graph['message'])
    response.envelope.message.query_graph = message.query_graph

    interpreter = ARAXQueryGraphInterpreter()
    interpreter.translate_to_araxi(response)
    if response.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response

    araxi_commands = response.data['araxi_commands']
    for cmd in araxi_commands:
        print(f"  - {cmd}")

    #### Show the final result
    #print('-------------------------')
    #print(response.show(level=ARAXResponse.DEBUG))
    #print(json.dumps(message.to_dict(),sort_keys=True,indent=2))

    #### Actually run the query
    from ARAX_query import ARAXQuery
    import ast
    araxq = ARAXQuery()
    # Run the query
    araxq.query({**input_query_graph, "operations": {"actions":araxi_commands}})
    # unpack the response
    response = araxq.response
    envelope = response.envelope
    message = envelope.message  # overrides the current message
    envelope.status = response.error_code
    envelope.description = response.message
    # return the message ID
    print(f"Returned response id: {envelope.id}")
    print('-------------------------')
    # print the whole message
    #print(json.dumps(ast.literal_eval(repr(envelope)), sort_keys=True, indent=2))
    # save message to file (since I can't get the UI working locally for some reason)
    with open('QGI_test6.json', 'w', encoding='utf-8') as f:
        json.dump(ast.literal_eval(repr(envelope)), f, ensure_ascii=False, indent=4)

def QGI_test7():
    # This is to test a three hop query with one end pinned (should result in FET ARAXi commands), and actually run the query
    input_query_graph = {
        "message": {
            "query_graph": {
                "edges": {
                    "e00": {
                        "object": "n01",
                        "subject": "n00"
                    },
                    "e01": {
                        "object": "n02",
                        "subject": "n01"
                    },
                    "e02": {
                        "object": "n03",
                        "subject": "n02"
                    }
                },
                "nodes": {
                    "n00": {
                        "categories": [
                            "biolink:ChemicalEntity"
                        ],
                        "ids": [
                            "DRUGBANK:DB00150"
                        ]
                    },
                    "n01": {
                        "categories": [
                            "biolink:Protein"
                        ]
                    },
                    "n02": {
                        "categories": [
                            "biolink:MolecularActivity"
                        ]
                    },
                    "n03": {
                        "categories": [
                            "biolink:ChemicalEntity"
                        ]
                    }
                }
            }

        }
    }

    #### Create a template Message
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    message = ARAXMessenger().from_dict(input_query_graph['message'])
    response.envelope.message.query_graph = message.query_graph

    interpreter = ARAXQueryGraphInterpreter()
    interpreter.translate_to_araxi(response)
    if response.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response

    araxi_commands = response.data['araxi_commands']
    for cmd in araxi_commands:
        print(f"  - {cmd}")

    #### Show the final result
    # print('-------------------------')
    # print(response.show(level=ARAXResponse.DEBUG))
    # print(json.dumps(message.to_dict(),sort_keys=True,indent=2))

    #### Actually run the query
    from ARAX_query import ARAXQuery
    import ast
    araxq = ARAXQuery()
    # Run the query
    araxq.query({**input_query_graph, "operations": {"actions": araxi_commands}})
    # unpack the response
    response = araxq.response
    envelope = response.envelope
    message = envelope.message  # overrides the current message
    envelope.status = response.error_code
    envelope.description = response.message
    # return the message ID
    print(f"Returned response id: {envelope.id}")
    print('-------------------------')
    # print the whole message
    # print(json.dumps(ast.literal_eval(repr(envelope)), sort_keys=True, indent=2))
    # save message to file (since I can't get the UI working locally for some reason)
    with open('QGI_test7.json', 'w', encoding='utf-8') as f:
        json.dump(ast.literal_eval(repr(envelope)), f, ensure_ascii=False, indent=4)
##########################################################################################
def main():

    import argparse

    argparser = argparse.ArgumentParser(description='Class for parsing an incoming query graph and deciding what to do')
    argparser.add_argument('test_number', type=str, nargs='*', help='Optional test to run')
    params = argparser.parse_args()

    #print(params.test_number)
    if params.test_number[0] == '2':
        QGI_test2()
    elif params.test_number[0] == '3':
        QGI_test3()
    elif params.test_number[0] == '4':
        QGI_test4()
    elif params.test_number[0] == '5':
        QGI_test5()
    elif params.test_number[0] == '6':
        QGI_test6()
    elif params.test_number[0] == '7':
        QGI_test7()
    else:
        QGI_test1()


if __name__ == "__main__": main()


# code/ARAX/ARAXQuery/ARAX_query_tracker.py

#!/usr/bin/python3

import sys
import os
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)
import time
import re
import signal
import socket

from datetime import datetime
import sqlalchemy
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, Float, String, DateTime, PickleType
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../")
from RTXConfiguration import RTXConfiguration

Base = declarative_base()

class ARAXQuery(Base):
    __tablename__ = 'arax_query'
    query_id = Column(Integer, primary_key=True)
    status = Column(String(255), nullable=False)
    start_datetime = Column(String(25), nullable=False) ## ISO formatted YYYY-MM-DD HH:mm:ss
    end_datetime = Column(String(25), nullable=True) ## ISO formatted YYYY-MM-DD HH:mm:ss
    elapsed = Column(Float, nullable=True) ## seconds
    pid = Column(Integer, nullable=False)
    domain = Column(String(255), nullable=True)
    hostname = Column(String(255), nullable=True)
    instance_name = Column(String(255), nullable=False)
    origin = Column(String(255), nullable=False)
    input_query = Column(PickleType, nullable=False) ## blob object
    message_id = Column(Integer, nullable=True)
    message_code = Column(String(255), nullable=True)
    code_description = Column(String(255), nullable=True)
    remote_address = Column(String(50), nullable=False)

class ARAXQueryTracker:

   #### Constructor
    def __init__(self):
        self.rtxConfig = RTXConfiguration()
        self.databaseName = "QueryTracker"
        self.engine_type = 'sqlite'
        self.session = None
        self.engine = None

        if self.rtxConfig.is_production_server:
            self.databaseName = "ResponseCache"
            self.engine_type = 'mysql'
        self.connect()

    def __del__(self):
        self.disconnect()

    #### Define attribute session
    @property
    def session(self) -> str:
        return self._session

    @session.setter
    def session(self, session: str):
        self._session = session


    #### Define attribute engine
    @property
    def engine(self) -> str:
        return self._engine

    @engine.setter
    def engine(self, engine: str):
        self._engine = engine


    #### Define attribute databaseName
    @property
    def databaseName(self) -> str:
        return self._databaseName

    @databaseName.setter
    def databaseName(self, databaseName: str):
        self._databaseName = databaseName


    ##################################################################################################
    def create_tables(self):
        Base.metadata.drop_all(self.engine)
        Base.metadata.create_all(self.engine)


    ##################################################################################################
    #### Create and store a database connection
    def connect(self):

        # If the engine_type is mysql then connect to the MySQL database
        if self.engine_type == 'mysql':
            engine = create_engine("mysql+pymysql://" + self.rtxConfig.mysql_feedback_username + ":" +
                self.rtxConfig.mysql_feedback_password + "@" + self.rtxConfig.mysql_feedback_host + "/" + self.databaseName)

        # Else just use SQLite
        else:
            database_path = os.path.dirname(os.path.abspath(__file__)) + '/' + self.databaseName + '.sqlite'
            engine = create_engine("sqlite:///"+database_path)

        DBSession = sessionmaker(bind=engine)
        session = DBSession()
        self.session = session
        self.engine = engine

        #### If the tables don't exist, then create the database
        database_info = sqlalchemy.inspect(engine)
        if not database_info.has_table(ARAXQuery.__tablename__):
            eprint(f"WARNING: {self.engine_type} tables do not exist; creating them")
            Base.metadata.create_all(engine)


    ##################################################################################################
    def disconnect(self):
        session = self.session
        engine = self.engine
        session.close()
        try:
            engine.dispose()
        except:
            pass


    ##################################################################################################
    #### Delete and create the ResponseStore database. Careful!
    def create_database(self):
        print("Creating database")

        # If the engine_type is mysql then set up the MySQL database
        if self.engine_type == 'mysql':
            engine = create_engine("mysql+pymysql://" + self.rtxConfig.mysql_feedback_username + ":" +
                self.rtxConfig.mysql_feedback_password + "@" + self.rtxConfig.mysql_feedback_host + "/" + self.databaseName)

        # Else just use SQLite
        else:
            database_path = os.path.dirname(os.path.abspath(__file__)) + '/' + self.databaseName + '.sqlite'
            if os.path.exists(database_path):
                os.remove(database_path)
            engine = create_engine("sqlite:///"+database_path)

        Base.metadata.drop_all(engine)
        Base.metadata.create_all(engine)
        self.connect()


    ##################################################################################################
    #### Create and store a database connection
    def connect(self):

        # If the engine_type is mysql then connect to the MySQL database
        if self.engine_type == 'mysql':
            engine = create_engine("mysql+pymysql://" + self.rtxConfig.mysql_feedback_username + ":" +
                self.rtxConfig.mysql_feedback_password + "@" + self.rtxConfig.mysql_feedback_host + "/" + self.databaseName)

        # Else just use SQLite
        else:
            return
            database_path = os.path.dirname(os.path.abspath(__file__)) + '/' + self.databaseName + '.sqlite'
            engine = create_engine("sqlite:///"+database_path)

        DBSession = sessionmaker(bind=engine)
        session = DBSession()
        self.session = session
        self.engine = engine

        #### If the tables don't exist, then create the database
        database_info = sqlalchemy.inspect(engine)
        if not database_info.has_table(ARAXQuery.__tablename__):
            eprint(f"WARNING: {self.engine_type} tables do not exist; creating them")
            Base.metadata.create_all(engine)


    ##################################################################################################
    #### Create and store a database connection
    def disconnect(self):
        session = self.session
        engine = self.engine
        if session is None:
            return
        try:
            session.close()
            engine.dispose()
        except:
            pass


    ##################################################################################################
    def update_tracker_entry(self, tracker_id, attributes):
        if tracker_id is None:
            return

        session = self.session
        if session is None:
            return

        tracker_entries = session.query(ARAXQuery).filter(ARAXQuery.query_id==tracker_id).all()
        if len(tracker_entries) > 0:
            tracker_entry = tracker_entries[0]
            end_datetime = datetime.now()
            elapsed = end_datetime - datetime.fromisoformat(tracker_entry.start_datetime)
            tracker_entry.end_datetime = end_datetime.isoformat(' ', 'seconds')
            tracker_entry.elapsed = elapsed.seconds
            tracker_entry.status = attributes['status'][:254]
            tracker_entry.message_id = attributes['message_id']
            tracker_entry.message_code = attributes['message_code'][:254]
            tracker_entry.code_description = attributes['code_description'][:254]
        session.commit()


    ##################################################################################################
    def alter_tracker_entry(self, tracker_id, attributes):
        if tracker_id is None:
            return

        session = self.session
        if session is None:
            return

        tracker_entries = session.query(ARAXQuery).filter(ARAXQuery.query_id==tracker_id).all()
        if len(tracker_entries) > 0:
            tracker_entry = tracker_entries[0]
            for key, value in attributes.items():
                setattr(tracker_entry, key, value)
        session.commit()


    ##################################################################################################
    def create_tracker_entry(self, attributes):
        session = self.session
        if session is None:
            return

        location = os.path.dirname(os.path.abspath(__file__))
        instance_name = '??'
        match = re.match(r'/mnt/data/orangeboard/(.+)/RTX/code', location)
        if match:
            instance_name = match.group(1)
        if instance_name == 'production':
            instance_name = 'ARAX'

        try:
            with open(location + '/../../config.domain') as infile:
                for line in infile:
                    domain = line.strip()
        except:
            domain = '??'

        hostname = socket.gethostname()

        try:
            tracker_entry = ARAXQuery(status="started",
                start_datetime=datetime.now().isoformat(' ', 'seconds'),
                pid=os.getpid(),
                domain = domain,
                hostname = hostname,
                instance_name = instance_name,
                origin=attributes['submitter'],
                input_query=attributes['input_query'],
                remote_address=attributes['remote_address'])
            session.add(tracker_entry)
            session.commit()
            tracker_id = tracker_entry.query_id
        except:
            tracker_id = 1
        return tracker_id

    ##################################################################################################
    def get_entries(self, last_n_hours=24, incomplete_only=False):
        if self.session is None:
            return

        if incomplete_only:
            return self.session.query(ARAXQuery).filter(
                text("""status NOT LIKE '%Completed%' 
                        AND TIMESTAMPDIFF(HOUR, STR_TO_DATE(start_datetime, '%Y-%m-%d %T'), NOW()) < :n""")).params(n=last_n_hours).all()
        else:
            if self.engine_type == "mysql":
                return self.session.query(ARAXQuery).filter(
                    text("""TIMESTAMPDIFF(HOUR, STR_TO_DATE(start_datetime, '%Y-%m-%d %T'), NOW()) < :n""")).params(n=last_n_hours).all()
            else:
                return self.session.query(ARAXQuery).filter(
                text("""JULIANDAY(start_datetime) - JULIANDAY(datetime('now','localtime')) < :n""")).params(n=last_n_hours/24).all()


    ##################################################################################################
    def get_status(self, last_n_hours=24, incomplete_only=False, id_=None):
        if self.session is None:
            return
        if last_n_hours is None or last_n_hours == 0:
            last_n_hours = 24

        if id_ is not None:
            return self.get_query_by_id(id_)

        entries = self.get_entries(last_n_hours=last_n_hours, incomplete_only=incomplete_only)
        result = { 'recent_queries': [], 'current_datetime': datetime.now().strftime("%Y-%m-%d %T") }
        if entries is None:
            return result

        for entry in entries:
            elapsed = entry.elapsed
            if elapsed is None or entry.status == 'Running Async':
                now = datetime.now()
                then = datetime.strptime(entry.start_datetime, '%Y-%m-%d %H:%M:%S')
                delta = now - then
                elapsed = int(delta.total_seconds())
                elapsed -= 1
                if elapsed < 0:
                    elapsed = 0
                eprint(f"--- {then} {elapsed}")
            result['recent_queries'].append( {
                'query_id': entry.query_id,
                'pid': entry.pid,
                'start_datetime': entry.start_datetime,
                'domain': entry.domain,
                'hostname': entry.hostname,
                'instance_name': entry.instance_name,
                'state': entry.status,
                'elapsed': elapsed,
                'submitter': entry.origin,
                'response_id': entry.message_id,
                'status': entry.message_code,
                'description': entry.code_description,
                'remote_address': entry.remote_address
            } )

        result['recent_queries'].reverse()
        result['current_datetime'] = datetime.now().strftime("%Y-%m-%d %T")
        return result


    ##################################################################################################
    def terminate_job(self, terminate_pid, authorization):
        if self.session is None:
            return { 'status': 'ERROR', 'description': 'Internal error ETJ500' }
        eprint(f"INFO: Entering terminate_job: pid={terminate_pid}, authorization={authorization}")
        reference_authorization = str(hash( 'Pickles' + str(terminate_pid)))
        if authorization is None or str(authorization) != reference_authorization:
            return { 'status': 'ERROR', 'description': 'Invalid authorization provided' }

        try:
            os.kill(terminate_pid, signal.SIGTERM)
        except:
            eprint(f"ERROR: Attempt to terminate pid={terminate_pid} failed")
            return { 'status': 'ERROR', 'description': f"ERROR: Attempt to terminate pid={terminate_pid} failed" }



        return { 'status': 'OK', 'description': f"Process {terminate_pid} terminated" }


    ####### ###########################################################################################
    def get_query_by_id(self, id_):
        if self.session is None:
            return

        if id_ is None:
            eprint(f"ERROR: query_id = {id}")
            return

        entries = self.session.query(ARAXQuery).filter(ARAXQuery.query_id == id_)
        for entry in entries:
            return entry.input_query

        eprint(f"ERROR: Unable to find query_id {id}")


    ####### ###########################################################################################
    def get_logs(self, mode='tail'):
        if self.session is None:
            return

        instance_name = self.get_instance_name()

        buffer = ''
        log_file = f"/tmp/RTX_OpenAPI_{instance_name}.elog"

        try:
            with open(log_file) as infile:
                for line in infile:
                    buffer += line
                return buffer
        except:
            buffer = f"ERROR: Unable to read log file '{log_file}'"


        eprint(buffer)
        return(buffer + "\n")


    ##################################################################################################
    def clear_unfinished_entries(self):
        if self.session is None:
            self.connect()
        if self.session is None:
            return

        instance_name = self.get_instance_name()

        entries = self.session.query(ARAXQuery).filter(ARAXQuery.instance_name == instance_name).filter( (ARAXQuery.elapsed == None) | (ARAXQuery.status == 'Running Async') )

        for entry in entries:
            eprint(f" - {entry.query_id}, {entry.instance_name}, {entry.elapsed}")
            now = datetime.now()
            then = datetime.strptime(entry.start_datetime, '%Y-%m-%d %H:%M:%S')
            delta = now - then
            elapsed = int(delta.total_seconds())
            entry.status = 'Reset'
            entry.message_code = 'Reset'
            entry.code_description = 'Query was terminated by a process restart'
            entry.elapsed = elapsed
        self.session.commit()


    ##################################################################################################
    def get_instance_name(self):
        location = os.path.abspath(__file__)
        instance_name = 'null'
        match = re.match(r'/mnt/data/orangeboard/(.+)/RTX/code', location)
        if match:
            instance_name = match.group(1)
        return instance_name


def main():

    query_tracker = ARAXQueryTracker()

    query_tracker.clear_unfinished_entries()
    return

    #query_tracker.create_database()

    if False:
        attributes = { 'origin': 'local_dev', 'input_query': { 'query_graph': { 'nodes': [], 'edges': [] } }, 'remote_address': 'test_address' }
        tracker_id = query_tracker.create_tracker_entry(attributes)

        time.sleep(1)
        attributes = { 'status': 'Completed OK', 'message_id': 3187, 'message_code': 'OK', 'code_description': '32 results' }
        query_tracker.update_tracker_entry(tracker_id, attributes)

    entries = query_tracker.get_entries(last_n_hours=24)
    for entry in entries:
        #print(entry.__dict__)
        print(f"{entry.query_id}\t{entry.pid}\t{entry.start_datetime}\t{entry.instance_name}\t{entry.status}\t{entry.elapsed}\t{entry.origin}\t{entry.message_id}\t{entry.message_code}\t{entry.code_description}")


if __name__ == "__main__":
    main()


# code/ARAX/ARAXQuery/ARAX_ranker.py

#!/bin/env python3
import math
import os
import networkx as nx
import numpy as np
import scipy.stats
import sys
import json
import ast
import re

from typing import Set, Union, Dict, List, Callable
from ARAX_response import ARAXResponse
from query_graph_info import QueryGraphInfo

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../UI/OpenAPI/python-flask-server/")
from openapi_server.models.query_graph import QueryGraph
from openapi_server.models.result import Result
from openapi_server.models.edge import Edge
from openapi_server.models.attribute import Attribute


def _get_nx_edges_by_attr(G: Union[nx.MultiDiGraph, nx.MultiGraph], key: str, val: str) -> Set[tuple]:
    res_set = set()
    for edge_tuple in G.edges(data=True):
        edge_val = edge_tuple[2].get(key, None)
        if edge_val is not None and edge_val == val:
            res_set.add(edge_tuple)
    return res_set


def _get_query_graph_networkx_from_query_graph(query_graph: QueryGraph) -> nx.MultiDiGraph:
    query_graph_nx = nx.MultiDiGraph()
    query_graph_nx.add_nodes_from([key for key,node in query_graph.nodes.items()])
    edge_list = [[edge.subject, edge.object, key, {'weight': 0.0}] for key,edge in query_graph.edges.items()]
    query_graph_nx.add_edges_from(edge_list)
    return query_graph_nx


def _get_weighted_graph_networkx_from_result_graph(kg_edge_id_to_edge: Dict[str, Edge],
                                                   qg_nx: Union[nx.MultiDiGraph, nx.MultiGraph],
                                                   result: Result) -> Union[nx.MultiDiGraph,
                                                                            nx.MultiGraph]:
    res_graph = qg_nx.copy()
    qg_edge_tuples = tuple(qg_nx.edges(keys=True, data=True))
    qg_edge_key_to_edge_tuple = {edge_tuple[2]: edge_tuple for edge_tuple in qg_edge_tuples}
    for key, edge_binding_list in result.edge_bindings.items():
        for edge_binding in edge_binding_list:
            kg_edge = kg_edge_id_to_edge[edge_binding.id]
            kg_edge_conf = kg_edge.confidence
            #kg_edge_conf = kg_edge_attributes["confidence"]
            qedge_keys = kg_edge.qedge_keys
            for qedge_key in qedge_keys:
                qedge_tuple = qg_edge_key_to_edge_tuple[qedge_key]
                res_graph[qedge_tuple[0]][qedge_tuple[1]][qedge_key]['weight'] += kg_edge_conf
    return res_graph


def _get_weighted_graphs_networkx_from_result_graphs(kg_edge_id_to_edge: Dict[str, Edge],
                                                     qg_nx: Union[nx.MultiDiGraph, nx.MultiGraph],
                                                     results: List[Result]) -> List[Union[nx.MultiDiGraph,
                                                                                          nx.MultiGraph]]:
    res_list = []
    for result in results:
        res_list.append(_get_weighted_graph_networkx_from_result_graph(kg_edge_id_to_edge,
                                                                       qg_nx,
                                                                       result))
    return res_list


# credit: StackOverflow:15590812
def _collapse_nx_multigraph_to_weighted_graph(graph_nx: Union[nx.MultiDiGraph,
                                                              nx.MultiGraph]) -> Union[nx.DiGraph,
                                                                                       nx.Graph]:
    if type(graph_nx) == nx.MultiGraph:
        ret_graph = nx.Graph()
    elif type(graph_nx) == nx.MultiDiGraph:
        ret_graph = nx.DiGraph()
    for u, v, data in graph_nx.edges(data=True):
        w = data['weight'] if 'weight' in data else 1.0
        if ret_graph.has_edge(u, v):
            ret_graph[u][v]['weight'] += w
        else:
            ret_graph.add_edge(u, v, weight=w)
    return ret_graph


# computes quantile ranks in *ascending* order (so a higher x entry has a higher
# "rank"), where ties have the same (average) rank (the reason for using scipy.stats
# here is specifically in order to handle ties correctly)
def _quantile_rank_list(x: List[float]) -> np.array:
    y = scipy.stats.rankdata(x, method='max')
    return y/len(y)


def _score_networkx_graphs_by_max_flow(result_graphs_nx: List[Union[nx.MultiDiGraph,
                                                                    nx.MultiGraph]]) -> List[float]:
    max_flow_values = []
    for result_graph_nx in result_graphs_nx:
        if len(result_graph_nx) > 1:
            apsp_dict = dict(nx.algorithms.shortest_paths.unweighted.all_pairs_shortest_path_length(result_graph_nx))
            path_len_with_pairs_list = [(node_i, node_j, path_len) for node_i, node_i_dict in apsp_dict.items() for node_j, path_len in node_i_dict.items()]
            max_path_len = max([path_len_with_pair_list_item[2] for path_len_with_pair_list_item in
                                path_len_with_pairs_list])
            pairs_with_max_path_len = [path_len_with_pair_list_item[0:2] for path_len_with_pair_list_item in path_len_with_pairs_list if
                                       path_len_with_pair_list_item[2] == max_path_len]
            max_flow_values_for_node_pairs = []
            result_graph_collapsed_nx = _collapse_nx_multigraph_to_weighted_graph(result_graph_nx)
            for source_node_id, target_node_id in pairs_with_max_path_len:
                max_flow_values_for_node_pairs.append(nx.algorithms.flow.maximum_flow_value(result_graph_collapsed_nx,
                                                                                            source_node_id,
                                                                                            target_node_id,
                                                                                            capacity="weight"))
            max_flow_value = 0.0
            if len(max_flow_values_for_node_pairs) > 0:
                max_flow_value = sum(max_flow_values_for_node_pairs)/float(len(max_flow_values_for_node_pairs))
        else:
            max_flow_value = 1.0
        max_flow_values.append(max_flow_value)
    return max_flow_values


def _score_networkx_graphs_by_longest_path(result_graphs_nx: List[Union[nx.MultiDiGraph,
                                                                        nx.MultiGraph]]) -> List[float]:
    result_scores = []
    for result_graph_nx in result_graphs_nx:
        apsp_dict = dict(nx.algorithms.shortest_paths.unweighted.all_pairs_shortest_path_length(result_graph_nx))
        path_len_with_pairs_list = [(node_i, node_j, path_len) for node_i, node_i_dict in apsp_dict.items() for node_j, path_len in node_i_dict.items()]
        max_path_len = max([path_len_with_pair_list_item[2] for path_len_with_pair_list_item in
                            path_len_with_pairs_list])
        pairs_with_max_path_len = [path_len_with_pair_list_item[0:2] for path_len_with_pair_list_item in path_len_with_pairs_list if
                                   path_len_with_pair_list_item[2] == max_path_len]
        map_node_name_to_index = {node_id: node_index for node_index, node_id in enumerate(result_graph_nx.nodes)}
        adj_matrix = nx.to_numpy_matrix(result_graph_nx)
        adj_matrix_power = np.linalg.matrix_power(adj_matrix, max_path_len)/math.factorial(max_path_len)
        score_list = [adj_matrix_power[map_node_name_to_index[node_i],
                                       map_node_name_to_index[node_j]] for node_i, node_j in pairs_with_max_path_len]
        result_score = np.mean(score_list)
        result_scores.append(result_score)
    return result_scores


def _score_networkx_graphs_by_frobenius_norm(result_graphs_nx: List[Union[nx.MultiDiGraph,
                                                                          nx.MultiGraph]]) -> List[float]:
    result_scores = []
    for result_graph_nx in result_graphs_nx:
        adj_matrix = nx.to_numpy_matrix(result_graph_nx)
        result_score = np.linalg.norm(adj_matrix, ord='fro')
        result_scores.append(result_score)
    return result_scores


def _score_result_graphs_by_networkx_graph_scorer(kg_edge_id_to_edge: Dict[str, Edge],
                                                  qg_nx: Union[nx.MultiDiGraph, nx.MultiGraph],
                                                  results: List[Result],
                                                  nx_graph_scorer: Callable[[List[Union[nx.MultiDiGraph,
                                                                                        nx.MultiGraph]]], np.array]) -> List[float]:
    result_graphs_nx = _get_weighted_graphs_networkx_from_result_graphs(kg_edge_id_to_edge,
                                                                        qg_nx,
                                                                        results)
    return nx_graph_scorer(result_graphs_nx)


class ARAXRanker:

    # #### Constructor
    def __init__(self):
        self.response = None
        self.message = None
        self.parameters = None
        # edge attributes we know about
        self.known_attributes = {'probability', 'normalized_google_distance', 'jaccard_index',
                                 'probability_treats', 'paired_concept_frequency',
                                 'observed_expected_ratio', 'chi_square', 'chi_square_pvalue', 'MAGMA-pvalue', 'Genetics-quantile',
                                 'pValue', 'fisher_exact_test_p-value','Richards-effector-genes',
                                 'feature_coefficient', 'CMAP similarity score'}
        # how much we trust each of the edge attributes
        self.known_attributes_to_trust = {'probability': 0.5,
                                          'normalized_google_distance': 0.8,
                                          'jaccard_index': 0.5,
                                          'probability_treats': 1,
                                          'paired_concept_frequency': 0.5,
                                          'observed_expected_ratio': 0.8,
                                          'chi_square': 0.8,
                                          'chi_square_pvalue': 0.8,
                                          'MAGMA-pvalue': 1.0,
                                          'Genetics-quantile': 1.0,
                                          'pValue': 1.0,
                                          'fisher_exact_test_p-value': 0.8,
                                          'Richards-effector-genes': 0.5,
                                          'feature_coefficient': 1.0,
                                          'CMAP similarity score': 1.0
                                          }
        self.virtual_edge_types = {}
        self.score_stats = dict()  # dictionary that stores that max's and min's of the edge attribute values
        self.kg_edge_id_to_edge = dict()  # map between the edge id's in the results and the actual edges themselves

    def describe_me(self):
        """
        Little helper function for internal use that describes the actions and what they can do
        :return:
        """

        brief_description = """
rank_results iterates through all edges in the results list aggrigating and 
normalizing the scores stored within the edge_attributes property. After combining these scores into 
one score the ranker then scores each result through a combination of max flow, longest path, 
and frobenius norm.
        """
        description = """
`rank_results` iterates through all edges in the results list aggrigating and 
normalizing the scores stored within the `edge_attributes` property. After combining these scores into 
one score the ranker then scores each result through a combination of 
[max flow](https://en.wikipedia.org/wiki/Maximum_flow_problem), 
[longest path](https://en.wikipedia.org/wiki/Longest_path_problem), 
and [frobenius norm](https://en.wikipedia.org/wiki/Matrix_norm#Frobenius_norm).
        """
        description_list = []
        params_dict = dict()
        params_dict['brief_description'] = brief_description
        params_dict['description'] = description
        params_dict["dsl_command"] = "rank_results()"
        description_list.append(params_dict)
        return description_list

    def result_confidence_maker(self, result):
        ###############################
        # old method of just multiplying ALL the edge confidences together
        if True:
            result_confidence = 1  # everybody gets to start with a confidence of 1
            for edge in result.edge_bindings:
                kg_edge_id = edge.kg_id
                # TODO: replace this with the more intelligent function
                # here we are just multiplying the edge confidences
                # --- to see what info is going into each result: print(f"{result.essence}: {kg_edges[kg_edge_id].type}, {kg_edges[kg_edge_id].confidence}")
                result_confidence *= self.kg_edge_id_to_edge[kg_edge_id].confidence
                #kg_edge_attributes = {x.original_attribute_name:x.value for x in self.kg_edge_id_to_edge[kg_edge_id].attributes}
                #result_confidence *= kg_edge_attributes["confidence"]
            result.confidence = result_confidence
        else:
            # consider each pair of nodes in the QG, then somehow combine that information
            # Idea:
            #   in each result
            #       for each source and target node:
            #           combine the confidences into a single edge with a single confidence that takes everything into account (
            #           edges, edge scores, edge types, etc)
            #       then assign result confidence as average/median of these "single" edge confidences?
            result.confidence = 1

    def edge_attribute_score_combiner(self, edge):
        """
        This function takes a single edge and decides how to combine its attribute scores into a single confidence
        Eventually we will want
        1. To weight different attributes by different amounts
        2. Figure out what to do with edges that have no attributes
        """
        # Currently a dead simple "just multiply them all together"
        edge_confidence = 1
        edge_attribute_dict = {}
        if edge.attributes is not None:
            for edge_attribute in edge.attributes:
                if edge_attribute.original_attribute_name is not None:
                    edge_attribute_dict[edge_attribute.original_attribute_name] = edge_attribute.value
                    normalized_score = self.edge_attribute_score_normalizer(edge_attribute.original_attribute_name, edge_attribute.value)
                else:
                    edge_attribute_dict[edge_attribute.attribute_type_id] = edge_attribute.value
                    normalized_score = self.edge_attribute_score_normalizer(edge_attribute.attribute_type_id, edge_attribute.value)
                if normalized_score == -1:  # this means we have no current normalization of this kind of attribute,
                    continue  # so don't do anything to the score since we don't know what to do with it yet
                else:  # we have a way to normalize it, so multiply away
                    edge_confidence *= normalized_score
        if edge.attributes and any(attribute.attribute_type_id == "biolink:knowledge_source" and
                                   attribute.value == "infores:semmeddb" for attribute in edge.attributes):
            if edge_attribute_dict.get("biolink:publications", None) is not None:
                n_publications = len(edge_attribute_dict["biolink:publications"])
            else:
                n_publications = 0
            if n_publications == 0:
                pub_value = 0.01
            else:
                pub_value = np.log(n_publications)
                max_value = 1.0
                curve_steepness = 3.16993
                logistic_midpoint = 1.38629
                pub_value = max_value / float(1 + np.exp(-curve_steepness * (pub_value - logistic_midpoint)))
            edge_confidence *= pub_value
        return edge_confidence

    def edge_attribute_score_normalizer(self, edge_attribute_name: str, edge_attribute_value) -> float:
        """
        Takes an input edge attribute and value, dispatches it to the appropriate method that translates the value into
        something in the interval [0,1] where 0 is worse and 1 is better
        """
        if edge_attribute_name not in self.known_attributes:
            return -1  # TODO: might want to change this
        else:
            if edge_attribute_value == "no value!":
                edge_attribute_value = 0
            try:
                # check to see if it's convertible to a float (will catch None's as well)
                edge_attribute_value = float(edge_attribute_value)
            except TypeError:
                return 0.
            except ValueError:
                return 0.
            # check to see if it's NaN, if so, return 0
            if np.isnan(edge_attribute_value):
                return 0.
            # else it's all good to proceed
            else:
                # Fix hyphens or spaces to underscores in names
                edge_attribute_name = re.sub(r'[- \:]','_',edge_attribute_name)
                # then dispatch to the appropriate function that does the score normalizing to get it to be in [0, 1] with 1 better
                return getattr(self, '_' + self.__class__.__name__ + '__normalize_' + edge_attribute_name)(value=edge_attribute_value)

    def __normalize_probability_treats(self, value):
        """
        Normalize the probability drug treats disease value.
        Empirically we've found that values greater than ~0.75 are "good" and <~0.75 are "bad" predictions of "treats"
        We will hence throw this in a logistic function so that higher scores remain high, and low scores drop off
        pretty quickly.
        To see this curve in Mathematica:
        L = 1;  (*max value returned*)
        k = 15;  (*steepness of the logistic curve*)
        x0 = 0.60;  (*mid point of the logistic curve*)
        Plot[L/(1 + Exp[-k (x - x0)]), {x, 0, 1}, PlotRange -> All, AxesOrigin -> {0, 0}]
        or
        import matplotlib.pyplot as plt
        max_value = 1
        curve_steepness = 15
        logistic_midpoint = 0.60
        x = np.linspace(0,1,200)
        y = [max_value / float(1+np.exp(-curve_steepness*(value - logistic_midpoint))) for value in x]
        plt.plot(x,y)
        plt.show()
        """
        max_value = 1
        curve_steepness = 15
        logistic_midpoint = 0.60
        normalized_value = max_value / float(1+np.exp(-curve_steepness*(value - logistic_midpoint)))
        # TODO: if "near" to the min value, set to zero (maybe one std dev from the min value of the logistic curve?)
        # TODO: make sure max value can be obtained
        return normalized_value

    def __normalize_normalized_google_distance(self, value):
        """
        Normalize the "normalized_google_distance
        """
        max_value = 1
        curve_steepness = -9
        logistic_midpoint = 0.60
        normalized_value = max_value / float(1 + np.exp(-curve_steepness * (value - logistic_midpoint)))
        # TODO: if "near" to the min value, set to zero (maybe one std dev from the min value of the logistic curve?)
        # TODO: make sure max value can be obtained
        return normalized_value

    def __normalize_probability(self, value):
        """
        These (as of 7/28/2020 in KG1 and KG2 only "drug->protein binding probabilities"
        As Vlado suggested, the lower ones are more rubbish, so again throw into a logistic function, but even steeper.
        see __normalize_probability_treats for how to visualize this
        """
        max_value = 1
        curve_steepness = 20
        logistic_midpoint = 0.8
        normalized_value = max_value / float(1 + np.exp(-curve_steepness * (value - logistic_midpoint)))
        # TODO: if "near" to the min value, set to zero (maybe one std dev from the min value of the logistic curve?)
        # TODO: make sure max value can be obtained
        return normalized_value

    def __normalize_jaccard_index(self, value):
        """
        The jaccard index is all relative to other results, so there is no reason to use a logistic here.
        Just compare the value to the maximum value
        """
        normalized_value = value / self.score_stats['jaccard_index']['maximum']
        # print(f"value: {value}, normalized: {normalized_value}")
        return normalized_value

    def __normalize_paired_concept_frequency(self, value):
        """
        Again, these are _somewhat_ relative values. In actuality, a logistic here would make sense,
        but I don't know the distribution of frequencies in COHD, so just go the relative route
        """
        # check to make sure we don't divide by zero
        # try:
        #    normalized_value = value / score_stats['paired_concept_frequency']['maximum']
        # except ZeroDivisionError:
        #    normalized_value = value / (score_stats['paired_concept_frequency']['maximum'] + np.finfo(float).eps)

        # Give logistic a try
        # TODO: see if we can adjust these params based on the scores stats (or see if that's even a good idea)
        max_value = 1
        curve_steepness = 2000  # really steep since the max values I've ever seen are quite small (eg .03)
        logistic_midpoint = 0.002  # seems like an ok mid point, but....
        normalized_value = max_value / float(1 + np.exp(-curve_steepness * (value - logistic_midpoint)))
        # TODO: if "near" to the min value, set to zero (maybe one std dev from the min value of the logistic curve?)
        # TODO: make sure max value can be obtained
        # print(f"value: {value}, normalized: {normalized_value}")
        return normalized_value

    def __normalize_observed_expected_ratio(self, value):
        """
        These are log ratios so should be interpreted as Exp[value] times more likely than chance
        """
        max_value = 1
        curve_steepness = 2  # Todo: need to fiddle with this as it's not quite weighting things enough
        logistic_midpoint = 2  # Exp[2] more likely than chance
        normalized_value = max_value / float(1 + np.exp(-curve_steepness * (value - logistic_midpoint)))
        # TODO: if "near" to the min value, set to zero (maybe one std dev from the min value of the logistic curve?)
        # TODO: make sure max value can be obtained
        # print(f"value: {value}, normalized: {normalized_value}")
        return normalized_value


    def __normalize_chi_square(self, value):
        """
        From COHD: Note that due to large sample sizes, the chi-square can become very large.
        Hence the p-values will be very, very small... Hard to use logistic function, so instead, take the
        -log(p_value) approach and use that (taking a page from the geneticist's handbook)
        """
        # Taking value as is:
        # max_value = 1
        # curve_steepness = -100
        # logistic_midpoint = 0.05
        # normalized_value = max_value / float(1 + np.exp(-curve_steepness * (value - logistic_midpoint)))

        # -Log[p_value] approach
        value = -np.log(value)
        max_value = 1
        curve_steepness = 0.03
        logistic_midpoint = 200
        normalized_value = max_value / float(1 + np.exp(-curve_steepness * (value - logistic_midpoint)))
        # TODO: if "near" to the min value, set to zero (maybe one std dev from the min value of the logistic curve?)
        # TODO: make sure max value can be obtained
        # print(f"value: {value}, normalized: {normalized_value}")
        return normalized_value

    def __normalize_chi_square_pvalue(self, value):
        return self.__normalize_chi_square(value)

    def __normalize_MAGMA_pvalue(self, value):
        """
        For Genetics Provider MAGMA p-value: Convert provided p-value to a number between 0 and 1
        with 1 being best. Estimated conversion from SAR and DMK 2020-09-22
        """

        value = -np.log(value)
        max_value = 1.0
        curve_steepness = 0.849
        logistic_midpoint = 4.97
        normalized_value = max_value / float(1 + np.exp(-curve_steepness * (value - logistic_midpoint)))
        return normalized_value

    def __normalize_pValue(self, value):
        """
        For Genetics Provider MAGMA p-value: Convert provided p-value to a number between 0 and 1
        with 1 being best. Estimated conversion from SAR and DMK 2020-09-22
        """

        value = -np.log(value)
        max_value = 1.0
        curve_steepness = 0.849
        logistic_midpoint = 4.97
        normalized_value = max_value / float(1 + np.exp(-curve_steepness * (value - logistic_midpoint)))
        return normalized_value


    def __normalize_Genetics_quantile(self, value):
        """
        For Genetics Provider MAGMA quantile: We decide 2020-09-22 that just using
        the quantile as-is is best. With DMK, SAR, EWD.
        """

        return value

    def __normalize_fisher_exact_test_p_value(self, value):
        """
        For FET p-values: Including two options
        The first option is to simply use 1-(p-value)
        The second is a custom logorithmic. 0.05 should correspond to ~0.95 after the logistic is applied.
        """

        # option 1:
        # normalized_value = 1-value

        # option 2:
        try:
            if value <= np.finfo(float).eps:
                normalized_value = 1.
            else:
                value = -np.log(value)
                max_value = 1.0
                curve_steepness = 3
                logistic_midpoint = 2.7
                normalized_value = max_value / float(1 + np.exp(-curve_steepness * (value - logistic_midpoint)))
        except RuntimeWarning:  # this is the case when value is 0 (or nearly so), so should award the max value
            normalized_value = 1.

        return normalized_value

    def __normalize_CMAP_similarity_score(self, value):
        normalized_value = abs(value/100)
        return normalized_value

    def __normalize_Richards_effector_genes(self, value):
        return value

    def __normalize_feature_coefficient(self, value):
        log_abs_value = np.log(abs(value))
        max_value = 1
        curve_steepness = 2.75
        logistic_midpoint = 0.15
        normalized_value = max_value / float(1+np.exp(-curve_steepness*(log_abs_value - logistic_midpoint)))
        return normalized_value

    def aggregate_scores_dmk(self, response):
        """
        Take in a message,
        decorate all edges with confidences,
        take each result and use edge confidences and other info to populate result confidences,
        populate the result.row_data and message.table_column_names
        Does everything in place (no result returned)
        """
        self.response = response
        response.debug(f"Starting to rank results")
        message = response.envelope.message
        self.message = message

        # #### Compute some basic information about the query_graph
        query_graph_info = QueryGraphInfo()
        result = query_graph_info.assess(message)
        # response.merge(result)
        # if result.status != 'OK':
        #     print(response.show(level=ARAXResponse.DEBUG))
        #     return response

        # DMK FIXME: This need to be refactored so that:
        #    1. The attribute names are dynamically mapped to functions that handle their weightings (for ease of renaming attribute names)
        #    2. Weighting of individual attributes (eg. "probability" should be trusted MUCH less than "probability_treats")
        #    3. Auto-handling of normalizing scores to be in [0,1] (eg. observed_expected ration \in (-inf, inf) while probability \in (0,1)
        #    4. Auto-thresholding of values (eg. if chi_square <0.05, penalize the most, if probability_treats < 0.8, penalize the most, etc.)
        #    5. Allow for ranked answers (eg. observed_expected can have a single, huge value, skewing the rest of them

        # #### Iterate through all the edges in the knowledge graph to:
        # #### 1) Create a dict of all edges by id
        # #### 2) Collect some min,max stats for edge_attributes that we may need later
        kg_edge_id_to_edge = self.kg_edge_id_to_edge
        score_stats = self.score_stats
        no_non_inf_float_flag = True
        for edge_key,edge in message.knowledge_graph.edges.items():
            kg_edge_id_to_edge[edge_key] = edge
            if edge.attributes is not None:
                for edge_attribute in edge.attributes:
                    for attribute_name in self.known_attributes:
                        if edge_attribute.original_attribute_name == attribute_name or edge_attribute.attribute_type_id == attribute_name:
                            if edge_attribute.value == "no value!":
                                edge_attribute.value = 0
                                value = 0
                            else:
                                try:
                                    value = float(edge_attribute.value)
                                except ValueError:
                                    continue
                                except TypeError:
                                    continue
                            # initialize if not None already
                            if attribute_name not in score_stats:
                                score_stats[attribute_name] = {'minimum': None, 'maximum': None}  # FIXME: doesn't handle the case when all values are inf|NaN
                            if not np.isinf(value) and not np.isinf(-value) and not np.isnan(value):  # Ignore inf, -inf, and nan
                                no_non_inf_float_flag = False
                                if not score_stats[attribute_name]['minimum']:
                                    score_stats[attribute_name]['minimum'] = value
                                if not score_stats[attribute_name]['maximum']:
                                    score_stats[attribute_name]['maximum'] = value
                                if value > score_stats[attribute_name]['maximum']:
                                    score_stats[attribute_name]['maximum'] = value
                                if value < score_stats[attribute_name]['minimum']:
                                    score_stats[attribute_name]['minimum'] = value

        if no_non_inf_float_flag:
            response.warning(
                        f"No non-infinite value was encountered in any edge attribute in the knowledge graph.")
        response.info(f"Summary of available edge metrics: {score_stats}")

        # Loop over the entire KG and normalize and combine the score of each edge, place that information in the confidence attribute of the edge
        for edge_key,edge in message.knowledge_graph.edges.items():
            if edge.attributes is not None:
                edge_attributes = {x.original_attribute_name:x.value for x in edge.attributes}
            else:
                edge_attributes = {}
            if edge_attributes.get("confidence", None) is not None:
            #if False:       # FIXME: there is no longer such an attribute. Stored as a generic attribute?
            #if edge.confidence is not None:
                # don't touch the confidence, since apparently someone already knows what the confidence should be
                edge.confidence = edge_attributes['confidence']
                #continue
            else:
                confidence = self.edge_attribute_score_combiner(edge)
                #edge.attributes.append(Attribute(name="confidence", value=confidence))
                edge.confidence = confidence

        # Now that each edge has a confidence attached to it based on it's attributes, we can now:
        # 1. consider edge types of the results
        # 2. number of edges in the results
        # 3. possibly conflicting information, etc.

        ###################################
        # TODO: Replace this with a more "intelligent" separate function
        # now we can loop over all the results, and combine their edge confidences (now populated)
        qg_nx = _get_query_graph_networkx_from_query_graph(message.query_graph)
        kg_edge_id_to_edge = self.kg_edge_id_to_edge
        results = message.results
        ranks_list = list(map(_quantile_rank_list,
                              map(lambda scorer_func: _score_result_graphs_by_networkx_graph_scorer(kg_edge_id_to_edge,
                                                                                                    qg_nx,
                                                                                                    results,
                                                                                                    scorer_func),
                                  [_score_networkx_graphs_by_max_flow,
                                   _score_networkx_graphs_by_longest_path,
                                   _score_networkx_graphs_by_frobenius_norm])))
        #print(ranks_list)
        #print(float(len(ranks_list)))
        result_scores = sum(ranks_list)/float(len(ranks_list))
        #print(result_scores)
        for result, score in zip(results, result_scores):
            result.score = score

        # for result in message.results:
        #     self.result_confidence_maker(result)
        ###################################

            # Make all scores at least 0.001. This is all way low anyway, but let's not have anything that rounds to zero
            # This is a little bad in that 0.0005 becomes better than 0.0011, but this is all way low, so who cares
            if result.score < 0.001:
                result.score += 0.001

            # Round to reasonable precision. Keep only 3 digits after the decimal
            score = int(result.score * 1000 + 0.5) / 1000.0

            result.row_data = [score, result.essence, result.essence_category]

        # Add table columns name
        response.envelope.table_column_names = ['score', 'essence', 'essence_category']

        # Re-sort the final results
        message.results.sort(key=lambda result: result.score, reverse=True)
        response.debug("Results have been ranked and sorted")



##########################################################################################


def main():
    # For faster testing, cache the testing messages locally
    import requests_cache
    requests_cache.install_cache('ARAX_ranker_testing_cache')

    import argparse
    argparser = argparse.ArgumentParser(description='Ranker system')
    argparser.add_argument('--local', action='store_true', help='If set, use local RTXFeedback database to fetch messages')
    params = argparser.parse_args()

    # --- Create a response object
    response = ARAXResponse()
    ranker = ARAXRanker()

    # --- Get a Message to work on
    from ARAX_messenger import ARAXMessenger
    messenger = ARAXMessenger()
    if not params.local:
        print("INFO: Fetching message to work on from arax.ncats.io", flush=True)
        message = messenger.fetch_message('https://arax.ncats.io/api/rtx/v1/message/2614')  # acetaminophen - > protein, just NGD as virtual edge
        # message = messenger.fetch_message('https://arax.ncats.io/api/rtx/v1/message/2687')  # neutropenia -> drug, predict_drug_treats_disease and ngd
        # message = messenger.fetch_message('https://arax.ncats.io/api/rtx/v1/message/2701') # observed_expected_ratio and ngd
        # message = messenger.fetch_message('https://arax.ncats.io/api/rtx/v1/message/2703')  # a huge one with jaccard
        # message = messenger.fetch_message('https://arax.ncats.io/api/rtx/v1/message/2706')  # small one with paired concept frequency
        # message = messenger.fetch_message('https://arax.ncats.io/api/rtx/v1/message/2709')  # bigger one with paired concept frequency

    # For local messages due to local changes in code not rolled out to production:
    if params.local:
        sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../../UI/Feedback")
        from RTXFeedback import RTXFeedback
        araxdb = RTXFeedback()
        message_dict = araxdb.getMessage(294)  # local version of 2709 but with updates to COHD
        # message_dict = araxdb.getMessage(297)
        # message_dict = araxdb.getMessage(298)
        # message_dict = araxdb.getMessage(299)  # observed_expected_ratio different disease
        # message_dict = araxdb.getMessage(300)  # chi_square
        # message_dict = araxdb.getMessage(302)  # chi_square, different disease
        # message_dict = araxdb.getMessage(304)  # all clinical info, osteoarthritis
        # message_dict = araxdb.getMessage(305)  # all clinical info, neurtropenia
        # message_dict = araxdb.getMessage(306)  # all clinical info, neurtropenia, but with virtual edges
        # message_dict = araxdb.getMessage(307)  # all clinical info, osteoarthritis, but with virtual edges
        # message_dict = araxdb.getMessage(322)  # Parkinsons Jaccard, top 50
        # message_dict = araxdb.getMessage(324)  # chi_square, KG2
        # message_dict = araxdb.getMessage(325)  # chi_square, ngd, KG2
        # message_dict = araxdb.getMessage(326)  # prob drug treats disease as attribute to all edge thrombocytopenia
        # message_dict = araxdb.getMessage(327)
        # add_qnode(name=DOID:1227, id=n00)
        # add_qnode(type=protein, is_set=true, id=n01)
        # add_qnode(type=chemical_substance, id=n02)
        # add_qedge(subject=n00, object=n01, id=e00)
        # add_qedge(subject=n01, object=n02, id=e01, type=physically_interacts_with)
        # expand(edge_id=[e00,e01], kp=ARAX/KG1)
        # overlay(action=compute_jaccard, start_node_id=n00, intermediate_node_id=n01, end_node_id=n02, virtual_relation_label=J1)
        # overlay(action=predict_drug_treats_disease, source_qnode_id=n02, target_qnode_id=n00, virtual_relation_label=P1)
        # overlay(action=overlay_clinical_info, chi_square=true, virtual_relation_label=C1, source_qnode_id=n00, target_qnode_id=n02)
        # overlay(action=compute_ngd, virtual_relation_label=N1, source_qnode_id=n00, target_qnode_id=n01)
        # overlay(action=compute_ngd, virtual_relation_label=N2, source_qnode_id=n00, target_qnode_id=n02)
        # overlay(action=compute_ngd, virtual_relation_label=N3, source_qnode_id=n01, target_qnode_id=n02)
        # resultify(ignore_edge_direction=true)
        # filter_results(action=limit_number_of_results, max_results=100)
        from ARAX_messenger import ARAXMessenger
        message = ARAXMessenger().from_dict(message_dict)

    if message is None:
        print("ERROR: Unable to fetch message")
        return

    # ranker.aggregate_scores(message,response=response)
    ranker.aggregate_scores_dmk(message, response=response)

    # Show the final result
    print(response.show(level=ARAXResponse.DEBUG))
    print("Results:")

    for result in message.results:
        confidence = result.confidence
        if confidence is None:
            confidence = 0.0
        print("  -" + '{:6.3f}'.format(confidence) + f"\t{result.essence}")
    # print(json.dumps(message.to_dict(),sort_keys=True,indent=2))

    # Show the message number
    print(json.dumps(ast.literal_eval(repr(message.id)), sort_keys=True, indent=2))


if __name__ == "__main__":
    main()


# code/ARAX/ARAXQuery/ARAX_response.py

#!/usr/bin/env python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import datetime


class ARAXResponse:

    #### Class variables
    DEBUG = 10
    INFO = 20
    WARNING = 30
    ERROR = 40
    level_names = { 10: 'DEBUG', 20: 'INFO', 30: 'WARNING', 40: 'ERROR' }
    equal_or_greater_levels = {
        'DEBUG': { 'DEBUG': 1, 'INFO': 1, 'WARNING': 1, 'ERROR': 1 },
        'INFO': { 'INFO': 1, 'WARNING': 1, 'ERROR': 1 },
        'WARNING': { 'WARNING': 1, 'ERROR': 1 },
        'ERROR': { 'ERROR': 1 }
        }
    output = None
    #output = 'STDERR'

    #### Constructor
    def __init__(self, status='OK', logging_level=WARNING, error_code='OK', message='Normal completion'):
        self.status = status
        self.http_status = 200
        self.logging_level = logging_level
        self.error_code = error_code
        self.message = message
        self.messages = []
        self.n_messages = 0
        self.n_errors = 0
        self.n_warnings = 0
        self.data = {}
        self.envelope = None

        self.query_plan = { 'qedge_keys': {}, 'counter': 0 }


    #### Add a debugging message
    def debug(self, message, code=None):
        """Public method that adds a DEBUG level message to the response object logger.
        DEBUG level messages should only be of interest to code developers.

        :param message: A natural English statement describing ongoing events.
        :type message: str
        """

        if code is None:
            code = ''
        self._add_message( message, self.DEBUG, code=code )


    #### Add an info message
    def info(self, message, code=None):
        """Public method that adds an INFO level message to the response object logger.
        INFO level messages should be of interest to ordinary users regarding the
        inner workings of the process or about innocuous assumptions made.

        :param message: A natural English statement describing ongoing events.
        :type message: str
        """

        if code is None:
            code = ''
        self._add_message( message, self.INFO, code=code )


    #### Add a warning message
    def warning(self, message, code=None):
        """Public method that adds a WARNING level message to the response object logger.
        WARNING level messages should be seen by ordinary users usually because
        crucial assumption was made or some subtask could not be successfully completed,
        although execution will continue as best as possible.

        :param message: A natural English statement describing an important event.
        :type message: str
        """

        if code is None:
            code = ''
        self._add_message( message, self.WARNING, code=code )
        self.n_warnings += 1


    #### Add an error message
    def error(self, message, code='UnknownError', error_code=None, http_status=400):
        """Public method that adds an ERROR level message to the response object logger.
        ERROR level messages must be conveyed to ordinary users usually because a
        crucial subtask could not be successfully completed and successful execution
        of the entire request cannot be completed. Note that logging an ERROR does
        not halt execution. Execution may continue for a while and multiple ERRORs may
        be logged, but at some point, execution should be halted and all logged events
        should be returned to the user for inspection and triage. The response object
        status attribute is automatically set to ERROR.

        :param message: A natural English statement describing a critical error.
        :type message: str
        :param code: A terse, unique string identifying the error (e.g. 'FileNotFound').
        :type code: str
        :param http_status: Integer HTTP status code to return (e.g. 400, 404, 500, 200).
        :type http_status: str
        """

        # Some backwards compatibility
        if error_code is not None and code == 'UnknownError':
            code = error_code

        self._add_message( message, self.ERROR, code=code )
        self.n_errors += 1
        self.status = 'ERROR'
        self.http_status = http_status
        self.error_code = code
        self.message = message


    #### Add a message
    def _add_message(self, message, level, code=None):
        """Private method called by the public methods to actually add the message to the log.

        :param message: A natural English statement describing the message.
        :type message: str
        :param level: One of the four numerical levels (i.e. 10, 20, 30, 40).
        :type level: int
        :param code: A terse machine-readable and human-readable code (e.g, KPNotAvailable).
        :type code: str
        """

        timestamp = str(datetime.datetime.now().isoformat())
        self.messages.append( { 'timestamp': timestamp, 'level': self.level_names[level], 'code': code, 'message': message } )
        self.n_messages += 1

        # Create a pretty printable message prefix
        prefix = f"{timestamp} {self.level_names[level]}: "
        if code is not None:
            prefix += f"[{code}] "

        if self.output is not None:
            if self.output == 'STDOUT':
                print(f"{prefix}{message}", flush=True)
            if self.output == 'STDERR':
                eprint(f"{prefix}{message}", flush=True)


    #### Merge a new response into an existing response
    def merge(self, response_to_merge):
        """Public method that merges the content of the passed response to the self response
        When the result of a called object method returns a new response object, this method
        should be used to merge the contents of the returned response object into the
        current response object. If the passed response is in and ERROR state, the current
        response is also set to an error state.

        :param response_to_merge: A response object received by the caller to be merged into the callers response object.
        :type response_to_merge: Response
        """
        self.n_messages += response_to_merge.n_messages
        self.n_errors += response_to_merge.n_errors
        self.n_warnings += response_to_merge.n_warnings
        for message in response_to_merge.messages:
            self.messages.append(message)
        if response_to_merge.status != 'OK':
            self.status = response_to_merge.status
            self.error_code = response_to_merge.error_code
            self.http_status = response_to_merge.http_status
            self.message = response_to_merge.message


    #### Return a text summary of the current state of the Response
    def show(self, level=WARNING):
        """Public method that returns a string buffer of a nice plain text rendering of the response object
        When the user or developer wants to see the current state of the response object,
        including all messages more severe that a certain level
        use print(response.show(level=response.INFO))

        :param level: Minimum message level to include in the string response (default: response.WARNING).
        :type level: integer
        :return: A string buffer (with newlines) suitable for plain-text printing
        :rtype: str
        """
        buffer = f"Response:\n"
        buffer += f"  status: {self.status}\n"
        buffer += f"  n_errors: {self.n_errors}  n_warnings: {self.n_warnings}  n_messages: {self.n_messages}\n"
        if self.status != 'OK':
            buffer += f"  error_code: {self.error_code}   message: {self.message}\n"
        for message in self.messages:
            if message['level'] in self.equal_or_greater_levels[self.level_names[level]]:

                # Create a pretty printable message prefix
                prefix = f"{message['timestamp']} {message['level']}: "
                if message['code'] is not None:
                    prefix += f"[{message['code']}] "

                buffer += f"  - {prefix}{message['message']}\n"
        return buffer


    #### Return the current list of messages as a formatted list
    def messages_list(self, level=WARNING):
        """Public method that returns a list of all messages greater or equal to the specified level
        Useful when a list of messages is sought
        e.g.: for message in response.message_list(level=response.INFO):

        :param level: Minimum message level to include in the returned list (default: response.WARNING).
        :type level: integer
        :return: A list of messages with timestamps and similar fancy things
        :rtype: list
        """
        result = []
        for message in self.messages:
            if message['level'] in self.equal_or_greater_levels[self.level_names[level]]:
                result.append(message)
        return result


    #### Add a query_plan element
    def update_query_plan(self, qedge_key, provider, status, description, query=None):
        """Method to add or update an element of the query_plan.

        :param edge_key: query_graph qedge key (e.g. 'e00').
        :type edge_key: str
        :param provider: knowledge provider name (e.g. 'infores:molepro).
        :type level: int
        :param status: status the KP (one of: Skipped, Waiting, Timed out, Error, Done).
        :type code: str
        :param description: Description of the result (see below for examples).
        :type code: str
        :param query: Optional query dict that is sent to the referenced KP.
        :type code: dict
        """

        """
            Suggested description lines:
                Skipped: does not support predicate 'biolink:physically_interacts_with'
                Query with 12 CURIEs sent: waiting for response
                Query with 4 CURIEs timed out after 30 seconds
                Query with 4 CURIEs returned ERROR after 1 seconds (HTTP 500)
                Query returned 89 results
        """

        if qedge_key not in self.query_plan['qedge_keys']:
            self.query_plan['qedge_keys'][qedge_key] = {}
        if provider == 'edge_properties':
            if provider not in self.query_plan['qedge_keys'][qedge_key]:
                self.query_plan['qedge_keys'][qedge_key][provider] = {}
            self.query_plan['qedge_keys'][qedge_key][provider][status] = description
        else:
            if provider not in self.query_plan['qedge_keys'][qedge_key]:
                self.query_plan['qedge_keys'][qedge_key][provider] = { 'status': status, 'description': description, 'query': query }
            else:
                self.query_plan['qedge_keys'][qedge_key][provider]['status'] = status
                self.query_plan['qedge_keys'][qedge_key][provider]['description'] = description
                if query is not None:
                    self.query_plan['qedge_keys'][qedge_key][provider]['query'] = query
        self.query_plan['counter'] += 1


##########################################################################################
import unittest
class ResponseTests(unittest.TestCase):

    def setUp(self):
        self.response = ARAXResponse()
        self.response.debug('And we are off')
        self.response.info('So far so good')
        self.response.warning('This does not look good')
        self.response.error('Bad news, Pal', code='BadNewsError')

    def test_n_errors(self):
        self.assertEqual(self.response.n_errors, 1)

    def test_n_warnings(self):
        self.assertEqual(self.response.n_warnings, 1)

    def test_n_messages(self):
        self.assertEqual(self.response.n_messages, 4)

    def test_messages_list(self):
        self.assertEqual(len(self.response.messages_list(level=self.response.DEBUG)), 4)

    def test_status(self):
        self.assertEqual(self.response.status, 'ERROR')

    def test_show(self):
        self.assertGreater(len(self.response.show(level=self.response.INFO)), 285)


##########################################################################################
def main():

    # ### Parse command line options
    import argparse
    argparser = argparse.ArgumentParser(description='Command line interface to the Response class. Runs tests when run without the --example=N flag')
    argparser.add_argument('--verbose', action='count', help='If set, print out messages to STDERR as they are generated' )
    argparser.add_argument('--example', type=int, help='Specify an example to run instead of unit tests (use --example=1)')
    params = argparser.parse_args()

    #### If no example was specified, run the unit tests
    if params.example is None:
        unittest.main()
        return

    #### Otherwise, we will run an example

    # ### Set verbosity
    # ### By default, all messages are not written out immediately, but just saved for later
    # ### Set class variable 'output' to STDERR or STDOUT to also write messages there
    # ### This is a **class** variable, so it applies to all instances of response objects!
    if params.verbose is not None:
        ARAXResponse.output = 'STDERR'

    #### Run a nice little example
    #### Create an Response object
    response = ARAXResponse()
    import json

    #### Set some messages
    response.debug('And we are off')
    response.info('So far so good')
    response.warning('This does not look good')
    response.error('Bad news, Pal', code='BadNewsError')

    #### Example for
    if params.example == 2:
        response.update_query_plan('e00', 'infores:molepro', 'Skipped', "KP does not support predicate 'biolink:physically_interacts_with'")
        response.update_query_plan('e00', 'infores:rtx-kg2', 'Done', "Query returned 89 results")
        response.update_query_plan('e00', 'infores:genetics-data-provider', 'Waiting', "Query with 12 CURIEs sent: waiting for response")
        print(json.dumps(response.query_plan,sort_keys=True,indent=2))
        return

    #### Show the resulting object including logged events at INFO or greater
    print(response.show(level=response.INFO))

    #### Request and print the full messages list at DEBUG and greater
    print("Messages list:")
    print(json.dumps(response.messages_list(level=response.DEBUG),sort_keys=True,indent=2))


if __name__ == "__main__": main()


# code/ARAX/ARAXQuery/ARAX_resultify.py

#!/usr/bin/env python3
'''This module defines the `ARAXResultify` class whose `_resultify` method
enumerates subgraphs of a knowledge graph (KG) that match a pattern set by a
query graph (QG) and sets the `results` data attribute of the `message` object
to be a list of `Result` objects, each corresponding to one of the enumerated
subgraphs. The matching between the KG subgraphs and the QG can be forced to be
sensitive to edge direction by setting `ignore_edge_direction=false` (the
default is to ignore edge direction).

   Usage: python3 -u ARAX_resultify.py

   will run the built-in tests for ARAX_resultify.py. When testing, also be sure
   to run the `document_dsl_commands.py` script in the `code/ARAX/Documentation`
   directory since that script uses the `describe_me` method of this module.

'''

import collections
import copy
import math
import os
import sys
from typing import List, Dict, Set, Union, Iterable, cast, Optional, Tuple, DefaultDict
from ARAX_response import ARAXResponse

__author__ = 'Stephen Ramsey and Amy Glen'
__copyright__ = 'Oregon State University'
__credits__ = ['Stephen Ramsey', 'Amy Glen', 'David Koslicki', 'Eric Deutsch']
__license__ = 'MIT'
__version__ = '0.1.0'
__maintainer__ = 'Amy Glen'
__email__ = ''
__status__ = 'Prototype'


# is there a better way to import swagger_server?  Following SO posting 16981921
PACKAGE_PARENT = '../../UI/OpenAPI/python-flask-server'
sys.path.append(os.path.normpath(os.path.join(os.getcwd(), PACKAGE_PARENT)))
from openapi_server.models.edge import Edge
from openapi_server.models.node import Node
from openapi_server.models.q_edge import QEdge
from openapi_server.models.q_node import QNode
from openapi_server.models.query_graph import QueryGraph
from openapi_server.models.knowledge_graph import KnowledgeGraph
from openapi_server.models.node_binding import NodeBinding
from openapi_server.models.edge_binding import EdgeBinding
from openapi_server.models.result import Result


class ARAXResultify:
    ALLOWED_PARAMETERS = {'debug', 'ignore_edge_direction'}

    def __init__(self):
        self.response = None
        self.message = None
        self.parameters = None

    def describe_me(self):
        """
        Little helper function for internal use that describes the actions and what they can do
        :return:
        """
        full_description = """ Creates a list of results from the input query graph (QG) based on the the
information contained in the message knowledge graph (KG). Every subgraph
through the KG that satisfies the GQ is returned. Such use cases include:
- `resultify()` Returns all subgraphs in the knowledge graph that satisfy the
  query graph
- `resultiy(ignore_edge_direction=false)` This mode checks edge directions in
the QG to ensure that matching an edge in the KG to an edge in the QG is only
allowed if the two edges point in the same direction. The default is to not
check edge direction. For example, you may want to include results that include
relationships like `(protein)-[involved_in]->(pathway)` even though the
underlying KG only contains directional edges of the form
`(protein)<-[involved_in]-(pathway)`.  Note that this command will successfully
execute given an arbitrary query graph and knowledge graph provided by the
automated reasoning system, not just ones generated by Team ARA Expander."""
        command_definition = {
            "dsl_command": "resultify()",
            "description": full_description,
            "brief_description": "Creates a list of results consisting of subgraphs from the message knowledge graph "
                                 "that satisfy the query graph.",
            "parameters": {
                "ignore_edge_direction": {
                    "is_required": False,
                    "examples": ["true", "false"],
                    "enum": ["true", "false", "True", "False", "t", "f", "T", "F"],
                    "default": "true",
                    "type": "boolean",
                    "description": "Whether to ignore (vs. obey) edge directions in the query graph when identifying "
                                   "paths that fulfill it.",
                }
            }
        }
        return [command_definition]

    def apply(self, response: ARAXResponse, input_parameters: dict) -> ARAXResponse:

        # Define a default response
        self.response = response
        self.message = response.envelope.message

        # Basic checks on arguments
        if not isinstance(input_parameters, dict):
            response.error("Provided parameters is not a dict", error_code="ParametersNotDict")
            return response

        # Return if any of the parameters generated an error (showing not just the first one)
        if response.status != 'OK':
            return response

        # Store these final parameters for convenience
        response.data['parameters'] = input_parameters
        self.parameters = input_parameters

        response.debug(f"Applying Resultifier to Message with parameters {input_parameters}")

        if not self.message.knowledge_graph:
            self.response.error(f"No message.knowledge_graph exists for resultify to create results from",
                                error_code="NoKG")
            return response

        # call _resultify
        self._resultify(describe=False)

        # Clean up the KG (should only contain nodes used in the results)
        self._clean_up_kg()

        # Return the response and done
        return response

    @staticmethod
    def recompute_qg_keys(response: ARAXResponse) -> ARAXResponse:
        """
        This method annotates nodes and edges in the KG with their respective qnode_keys and qedge_keys according to
        the node/edge bindings in the results. It is meant to be used on an already fully formed message (with results)
        that is being "imported", in a sense, so that ARAX can do further operations on it.
        """
        results = response.envelope.message.results
        kg = response.envelope.message.knowledge_graph
        qg = response.envelope.message.query_graph
        if not results:
            response.error(f"Cannot recompute QG keys for a message that has no results.", error_code="MissingResults")
            return response
        response.info(f"Recomputing QG keys (annotating nodes/edges in the KGs with their QG keys)")
        # First build a map of KG node/edges and their corresponding QG IDs
        kg_node_keys_to_qnode_keys = dict()
        kg_edge_keys_to_qedge_keys = dict()
        for result in results:
            for qnode_key, node_bindings in result.node_bindings.items():
                # FW: This is a hack to get reranking to work. might need to fix later
                if qnode_key not in qg.nodes:
                    for node_binding in node_bindings:
                        if node_binding.id in kg.nodes:
                            del kg.nodes[node_binding.id]
                    response.warning(f"While recomputing qnode keys found a node binding in the results without a corresponding query node. Removing the edge from the KG...")
                    continue
                for node_binding in node_bindings:
                    node_key = node_binding.id
                    if node_key not in kg_node_keys_to_qnode_keys:
                        kg_node_keys_to_qnode_keys[node_key] = set()
                    kg_node_keys_to_qnode_keys[node_key].add(qnode_key)
            for qedge_key, edge_bindings in result.edge_bindings.items():
                # FW: This is a hack to get reranking to work. might need to fix later
                if qedge_key not in qg.edges:
                    for edge_binding in edge_bindings:
                        if edge_binding.id in kg.edges:
                            del kg.edges[edge_binding.id]
                    response.warning(f"While recomputing qedge keys found a edge binding in the results without a corresponding query edge. Removing the edge from the KG...")
                    continue
                for edge_binding in edge_bindings:
                    edge_key = edge_binding.id
                    if edge_key not in kg_edge_keys_to_qedge_keys:
                        kg_edge_keys_to_qedge_keys[edge_key] = set()
                    kg_edge_keys_to_qedge_keys[edge_key].add(qedge_key)
        # Then go ahead and annotate the nodes/edges in the KG with their QG IDs
        for node_key, node in kg.nodes.items():
            if node_key not in kg_node_keys_to_qnode_keys:
                response.error(f"KG contains node(s) that do not appear in any results; cannot recompute their qnode_keys",
                               error_code="InvalidKG")
                return response
            node.qnode_keys = list(kg_node_keys_to_qnode_keys[node_key])
        for edge_key, edge in kg.edges.items():
            if edge_key not in kg_edge_keys_to_qedge_keys:
                response.error(f"KG contains edge(s) that do not appear in any results; cannot recompute their qedge_keys",
                               error_code="InvalidKG")
                return response
            edge.qedge_keys = list(kg_edge_keys_to_qedge_keys[edge_key])
        return response

    def _resultify(self, describe: bool = False):
        """From a knowledge graph and a query graph (both in a Message object), extract a list of Results objects, each containing
        lists of NodeBinding and EdgeBinding objects. Add a list of Results objects to self.message.rseults.

        It is required that `self.parameters` contain the following:
            ignore_edge_direction: a parameter of type `bool` indicating whether
            the direction of an edge in the knowledge graph should be taken into
            account when matching that edge to an edge in the query graph. By
            default, this parameter is `true`. Set this parameter to false in
            order to require that an edge in a subgraph of the KG will only
            match an edge in the QG if both have the same direction (taking into
            account the source/target node mapping). Optional.

        """
        assert self.response is not None
        results = self.message.results
        if results is not None and len(results) > 0:
            self.response.info(f"Clearing previous results and computing a new set of results")
            self.message.results = []
            results = self.message.results
            self.message.n_results = 0

        message = self.message
        parameters = self.parameters

        debug_mode = parameters.get('debug', None)
        if debug_mode is not None:
            try:
                debug_mode = _parse_boolean_case_insensitive(debug_mode)
            except Exception as e:
                self.response.error(str(e))
                return

        for parameter_name in parameters.keys():
            if parameter_name == '':
                continue
            if parameter_name not in ARAXResultify.ALLOWED_PARAMETERS:
                error_string = "parameter type is not allowed in ARAXResultify: " + str(parameter_name)
                if not debug_mode:
                    self.response.error(error_string)
                    return
                else:
                    raise ValueError(error_string)

        kg = message.knowledge_graph
        # Only resultify the portion of the QG that's been expanded #1848
        qg = _filter_to_expanded_portion(message.query_graph, self.response)
        # Ignore kryptonite ("not") edges/nodes in Resultify (Expand takes care of those) #1119
        qg = _get_qg_without_kryptonite_portions(qg)

        ignore_edge_direction = parameters.get('ignore_edge_direction', None)
        if ignore_edge_direction is not None:
            try:
                ignore_edge_direction = _parse_boolean_case_insensitive(ignore_edge_direction)
            except ValueError as e:
                error_string = "parameter value is not allowed in ARAXResultify: " + str(ignore_edge_direction)
                if not debug_mode:
                    self.response.error(error_string)
                    return
                else:
                    raise e

        # Actually create results
        results = _get_results_for_kg_by_qg(kg,
                                            qg,
                                            ignore_edge_direction,
                                            self.response)
        message_code = 'OK'
        code_description = 'Result list computed from KG and QG'

        message.results = results
        if len(results) == 0 and message_code == 'OK':
            message_code = 'WARNING'
            code_description = 'no results returned'
            if len(kg.nodes) == 0:
                code_description += '; empty knowledge graph'
            self.response.warning(code_description)

        message.n_results = len(results)
        message.code_description = code_description
        message.message_code = message_code

    def _clean_up_kg(self):
        self.response.debug(f"Cleaning up the KG to remove nodes not used in the results")
        results = self.message.results
        kg = self.message.knowledge_graph

        node_keys_used_in_results = {node_binding.id for result in results
                                     for node_binding_list in result.node_bindings.values()
                                     for node_binding in node_binding_list}
        cleaned_kg = KnowledgeGraph(nodes={node_key: node for node_key, node in kg.nodes.items()
                                           if node_key in node_keys_used_in_results},
                                    edges={edge_key: edge for edge_key, edge in kg.edges.items() if
                                           {edge.subject, edge.object}.issubset(node_keys_used_in_results)})
        self.message.knowledge_graph = cleaned_kg
        self.response.info(f"After cleaning, the KG contains {len(self.message.knowledge_graph.nodes)} nodes and "
                           f"{len(self.message.knowledge_graph.edges)} edges")


def _make_edge_key(node1_id: str,
                   node2_id: str) -> str:
    return node1_id + '->' + node2_id


def _is_specific_query_node(qnode_key: str, qnode: QNode):
    return (qnode_key is not None and ':' in qnode_key) or \
        (qnode.ids is not None and len(qnode.ids) > 0)


def _make_adj_maps(graph: Union[QueryGraph, KnowledgeGraph],
                   directed=True,
                   droploops=True) -> Dict[str, Dict[str, Set[str]]]:
    if directed:
        adj_map_in: Dict[str, Set[str]] = {node_key: set() for node_key in graph.nodes}
        adj_map_out: Dict[str, Set[str]] = {node_key: set() for node_key in graph.nodes}
    else:
        adj_map: Dict[str, Set[str]] = {node_key: set() for node_key in graph.nodes}
    try:
        for edge in graph.edges.values():
            if droploops and edge.object == edge.subject:
                continue
            if directed:
                edge_node_key = edge.subject
                adj_map_out[edge_node_key].add(edge.object)
                edge_node_key = edge.object
                adj_map_in[edge_node_key].add(edge.subject)
            else:
                edge_node_key = edge.subject
                adj_map[edge_node_key].add(edge.object)
                edge_node_key = edge.object
                adj_map[edge_node_key].add(edge.subject)
    except KeyError:
        raise ValueError("Graph has an edge " + str(edge) + " that refers to a node ID (" + edge_node_key + ") that is not in the graph")
    if directed:
        ret_dict = {'in': adj_map_in, 'out': adj_map_out}
    else:
        ret_dict = {'both': adj_map}
    return ret_dict


def _bfs_dists(adj_map: Dict[str, Set[str]],
               start_node_key: str) -> Dict[str, Union[int, float]]:
    queue = collections.deque([start_node_key])
    distances = {node_key: math.inf for node_key in adj_map.keys()}
    distances[start_node_key] = 0
    while len(queue) > 0:
        node_key = queue.popleft()
        node_dist = distances[node_key]
        assert not math.isinf(node_dist)
        for neighb_node_key in cast(Iterable[str], adj_map[node_key]):
            if math.isinf(distances[neighb_node_key]):
                distances[neighb_node_key] = node_dist + 1
                queue.append(neighb_node_key)
    return distances


def _get_essence_node_for_qg(qg: QueryGraph) -> Optional[str]:
    adj_map = _make_adj_maps(qg, directed=False)['both']
    node_keys_list = list(adj_map.keys())
    all_nodes = set(node_keys_list)
    node_degrees = list(map(len, adj_map.values()))
    leaf_nodes = set(node_keys_list[i] for i, k in enumerate(node_degrees) if k == 1)
    is_set_nodes = set(qnode_key for qnode_key, qnode in qg.nodes.items() if qnode.is_set)
    specific_nodes = set(qnode_key for qnode_key, qnode in qg.nodes.items() if _is_specific_query_node(qnode_key, qnode))
    non_specific_nodes = all_nodes - specific_nodes
    non_specific_leaf_nodes = leaf_nodes & non_specific_nodes

    candidate_essence_nodes = non_specific_leaf_nodes - is_set_nodes
    if len(candidate_essence_nodes) == 0:
        candidate_essence_nodes = non_specific_nodes - is_set_nodes
    if len(candidate_essence_nodes) == 0:
        return None
    elif len(candidate_essence_nodes) == 1:
        return next(iter(candidate_essence_nodes))
    else:
        specific_leaf_nodes = specific_nodes & leaf_nodes
        if len(specific_leaf_nodes) == 0:
            qnode_keys_list = [qnode_key for qnode_key in qg.nodes]
            # TODO: Now that qg.nodes is a dict, don't think there's any order to it.. is this still worthwhile?
            map_node_key_to_pos: Dict[str, Union[int, float]] = {qnode_key: i for i, qnode_key in enumerate(qnode_keys_list)}
            if len(specific_nodes) == 0:
                # return the node key of the non-specific node with the rightmost position in the QG node list
                return sorted(candidate_essence_nodes,
                              key=lambda node_key: map_node_key_to_pos[node_key],
                              reverse=True)[0]
            else:
                if len(specific_nodes) == 1:
                    specific_node_key = next(iter(specific_nodes))
                    return sorted(candidate_essence_nodes,
                                  key=lambda node_key: abs(map_node_key_to_pos[node_key] -
                                                          map_node_key_to_pos[specific_node_key]),
                                  reverse=True)[0]
                else:
                    # there are at least two non-specific leaf nodes and at least two specific nodes
                    return sorted(candidate_essence_nodes,
                                  key=lambda node_key: min([abs(map_node_key_to_pos[node_key] -
                                                               map_node_key_to_pos[specific_node_key]) for
                                                           specific_node_key in specific_nodes]),
                                  reverse=True)[0]
        else:
            if len(specific_leaf_nodes) == 1:
                specific_leaf_node_key = next(iter(specific_leaf_nodes))
                map_node_key_to_pos = _bfs_dists(adj_map, specific_leaf_node_key)
            else:
                all_dist_maps_for_spec_leaf_nodes = {node_key: _bfs_dists(adj_map,
                                                                         node_key) for
                                                     node_key in specific_leaf_nodes}
                qnode_keys_list = [qnode_key for qnode_key in qg.nodes]
                map_node_key_to_pos = {qnode_key: min([dist_map[qnode_key] for dist_map in all_dist_maps_for_spec_leaf_nodes.values()]) for
                                      qnode_key in qnode_keys_list}
            return sorted(candidate_essence_nodes,
                          key=lambda node_key: map_node_key_to_pos[node_key],
                          reverse=True)[0]
    assert False


def _parse_boolean_case_insensitive(input_string:  str) -> bool:
    if input_string is None:
        raise ValueError("invalid value for input_string")
    input_string = input_string.lower()
    if input_string in {"true", "t"}:
        return True
    elif input_string in {"false", "f"}:
        return False
    else:
        raise ValueError("invalid value for input_string")


def _get_qg_without_kryptonite_portions(qg: QueryGraph) -> QueryGraph:
    """
    This function returns a version of the query graph in which kryptonite ("not") qedges and their associated qnodes
    (that aren't otherwised used) have been removed. Resultify should work off of such a version of the QG (effectively
    ignoring kryptonite portions) because handling of kryptonite qedges is done upstream in Expand (see #1119).
    """
    kryptonite_qedge_keys = {qedge_key for qedge_key, qedge in qg.edges.items() if qedge.exclude}
    normal_qedge_keys = set(qg.edges).difference(kryptonite_qedge_keys)
    qnode_keys_used_by_kryptonite_qedges = {qnode_key for qedge_key in kryptonite_qedge_keys for qnode_key in
                                            [qg.edges[qedge_key].subject, qg.edges[qedge_key].object]}
    qnode_keys_used_by_normal_qedges = {qnode_key for qedge_key in normal_qedge_keys for qnode_key in
                                        [qg.edges[qedge_key].subject, qg.edges[qedge_key].object]}
    qnode_keys_used_only_by_kryptonite_qedges = qnode_keys_used_by_kryptonite_qedges.difference(qnode_keys_used_by_normal_qedges)
    return QueryGraph(nodes={qnode_key: qnode for qnode_key, qnode in qg.nodes.items() if qnode_key not in qnode_keys_used_only_by_kryptonite_qedges},
                      edges={qedge_key: qedge for qedge_key, qedge in qg.edges.items() if qedge_key in normal_qedge_keys})


def _filter_to_expanded_portion(qg: QueryGraph, log: ARAXResponse) -> QueryGraph:
    expanded_qedges = {qedge_key: qedge for qedge_key, qedge in qg.edges.items()
                       if hasattr(qedge, "filled") and qedge.filled}
    if not expanded_qedges:
        # If NO qedges have been marked as expanded but Resultify is being called, we'll assume this is either
        # an existing knowledge graph that was not created using Expand, or this is a single-node query.
        # In either case we should resultify the full QG.
        log.debug(f"No qedges were marked as expanded so we will resultify the entire QG")
        return qg
    else:
        # Otherwise we should only resultify the portion of the QG that's already been expanded
        expanded_qnodes = {qnode_key: qg.nodes[qnode_key] for qedge in expanded_qedges.values()
                           for qnode_key in {qedge.subject, qedge.object}}
        expanded_sub_qg = QueryGraph(edges=expanded_qedges, nodes=expanded_qnodes)
        log.debug(f"Expanded qedges are {set(expanded_qedges)}, expanded qnodes are {set(expanded_qnodes)}; "
                  f"will resultify only this sub-QG")
        return expanded_sub_qg


def _get_results_for_kg_by_qg(kg: KnowledgeGraph,              # all nodes *must* have qnode_key specified
                              qg: QueryGraph,
                              ignore_edge_direction: bool = True,
                              log: ARAXResponse = ARAXResponse()) -> List[Result]:

    if ignore_edge_direction is None:
        return _get_results_for_kg_by_qg(kg, qg, log=log)

    kg_node_keys_without_qnode_key = [node_key for node_key, node in kg.nodes.items() if not node.qnode_keys]
    if len(kg_node_keys_without_qnode_key) > 0:
        log.error("these node IDs do not have qnode_keys set: " + str(kg_node_keys_without_qnode_key), error_code="MissingQNodeKeys")
        return []

    kg_edge_keys_without_qedge_key = [edge_key for edge_key, edge in kg.edges.items() if not edge.qedge_keys]
    if len(kg_edge_keys_without_qedge_key) > 0:
        log.error("these edges do not have qedge_keys set: " + str(kg_edge_keys_without_qedge_key), error_code="MissingQEdgeKeys")
        return []

    kg_edge_keys_by_qg_key = _get_kg_edge_keys_by_qg_key(kg)
    kg_node_keys_by_qg_key = _get_kg_node_keys_by_qg_key(kg)

    # --------------------- checking for validity of the NodeBindings list --------------
    # we require that every query graph node ID in the "values" slot of the node_bindings_map corresponds to an actual node in the QG
    qnode_keys_mapped_that_are_not_in_qg = [qnode_key for qnode_key in kg_node_keys_by_qg_key if qnode_key not in qg.nodes]
    if len(qnode_keys_mapped_that_are_not_in_qg) > 0:
        log.error("A node in the KG has a qnode_key that does not exist in the QueryGraph: " + str(qnode_keys_mapped_that_are_not_in_qg),
                  error_code="UnknownQNodeKey")
        return []

    # --------------------- checking for validity of the EdgeBindings list --------------
    # we require that every query graph edge ID in the "values" slot of the edge_bindings_map corresponds to an actual edge in the QG
    qedge_keys_mapped_that_are_not_in_qg = [qedge_key for qedge_key in kg_edge_keys_by_qg_key if qedge_key not in qg.edges]
    if len(qedge_keys_mapped_that_are_not_in_qg) > 0:
        log.error("An edge in the KG has a qedge_key that does not exist in the QueryGraph: " + str(qedge_keys_mapped_that_are_not_in_qg),
                  error_code="UnknownQEdgeKey")
        return []

    # --------------------- checking that the source ID and target ID of every edge in KG is a valid KG node ---------------------
    node_keys_for_edges_that_are_not_valid_nodes = [edge.subject for edge in kg.edges.values() if edge.subject not in kg.nodes] + \
                                                   [edge.object for edge in kg.edges.values() if edge.object not in kg.nodes]
    if len(node_keys_for_edges_that_are_not_valid_nodes) > 0:
        log.error("KG has Edges that refer to the following non-existent Nodes: " + str(node_keys_for_edges_that_are_not_valid_nodes),
                  error_code="OrphanEdges")
        return []

    # --------------------- checking that the source ID and target ID of every edge in QG is a valid QG node ---------------------
    invalid_qnode_keys_used_by_qedges = [qedge.subject for qedge in qg.edges.values() if qedge.subject not in qg.nodes] + \
                                        [qedge.object for qedge in qg.edges.values() if qedge.subject not in qg.nodes]
    if len(invalid_qnode_keys_used_by_qedges) > 0:
        log.error("QG has QEdges that refer to the following non-existent QNodes: " + str(invalid_qnode_keys_used_by_qedges),
                  error_code="OrphanQEdges")
        return []

    # --------------------- checking for consistency of edge-to-node relationships, for all edge bindings -----------
    # check that for each bound KG edge, the QG mappings of the KG edges source and target nodes are also the
    # source and target nodes of the QG edge that corresponds to the bound KG edge
    for qedge_key, kg_edge_keys_for_this_qedge_key in kg_edge_keys_by_qg_key.items():
        qg_edge = qg.edges[qedge_key]
        qg_source_node_key = qg_edge.subject
        qg_target_node_key = qg_edge.object
        for edge_key in kg_edge_keys_for_this_qedge_key:
            kg_edge = kg.edges.get(edge_key)
            kg_source_node_key = kg_edge.subject
            kg_target_node_key = kg_edge.object
            if qg_source_node_key != qg_target_node_key:
                edge_valid_in_same_direction = (kg_source_node_key in kg_node_keys_by_qg_key[qg_source_node_key] and
                                                kg_target_node_key in kg_node_keys_by_qg_key[qg_target_node_key])
                edge_valid_in_opposite_direction = (kg_source_node_key in kg_node_keys_by_qg_key[qg_target_node_key] and
                                                    kg_target_node_key in kg_node_keys_by_qg_key[qg_source_node_key])
                edge_is_valid = (edge_valid_in_same_direction or edge_valid_in_opposite_direction) if ignore_edge_direction else edge_valid_in_same_direction
                if not edge_is_valid:
                    kg_source_node = kg.nodes.get(kg_source_node_key)
                    kg_target_node = kg.nodes.get(kg_target_node_key)
                    log.error(f"Edge {edge_key} (fulfilling {qedge_key}) has node(s) that do not fulfill the "
                              f"expected qnodes ({qg_source_node_key} and {qg_target_node_key}). Edge's nodes are "
                              f"{kg_source_node_key} (qnode_keys: {kg_source_node.qnode_keys}) and "
                              f"{kg_target_node_key} (qnode_keys: {kg_target_node.qnode_keys}).", error_code="MismatchedNodes")
                    return []

    # ------------------- checking to make sure option groups in QG are valid ---------------------
    # Qedges with an optional qnode must themselves be labeled optional
    optional_qnode_keys = {qnode_key for qnode_key, qnode in qg.nodes.items() if qnode.option_group_id}
    optional_qedge_keys = {qedge_key for qedge_key, qedge in qg.edges.items() if qedge.option_group_id}
    qedge_keys_with_optional_qnode = {qedge_key for qedge_key, qedge in qg.edges.items() if {qedge.subject, qedge.object}.intersection(optional_qnode_keys)}
    qedge_keys_missing_optional_label = qedge_keys_with_optional_qnode.difference(optional_qedge_keys)
    if qedge_keys_missing_optional_label:
        log.error(f"These qedges need to be labeled optional because they link to an optional qnode: "
                  f"{qedge_keys_missing_optional_label}", error_code="MissingOptionalLabel")
        return []

    # ============= save until SAR can discuss with {EWD,DMK} whether there can be unmapped nodes in the KG =============
    # # if any node in the KG is not bound to a node in the QG, drop the KG node; redefine "kg" as the filtered KG
    # kg_node_keys_keep = {node.id for node in kg.nodes if node.id in node_bindings_map}
    # kg_nodes_keep_list = [node for node in kg.nodes if node.id in kg_node_keys_keep]
    # kg_edges_keep_list = [edge for edge in kg.edges if not (edge.subject in kg_node_keys_keep and
    #                                                         edge.object in kg_node_keys_keep)]
    # kg = KnowledgeGraph(nodes=kg_nodes_keep_list,
    #                     edges=kg_edges_keep_list)
    # ============= save until SAR can discuss with {EWD,DMK} whether there can be unmapped nodes in the KG =============

    # Our goal is to enumerate all distinct "edge-maximal" subgraphs of the KG that each "covers"
    # the QG. A subgraph of KG that "covers" the QG is one for which all of the following conditions hold:
    # (1) under the KG-to-QG node bindings map, the range of the KG subgraph's nodes is the entire set of nodes in the QG
    # (2) for any QG node that has "is_set=True", *all* KG nodes that are bound to the same QG node are in the subgraph
    # (3) every edge in the QG is "covered" by at least one edge in the KG

    results: List[Result] = []

    subclass_clusters, child_to_parent_map = _get_subclass_clusters(kg_edge_keys_by_qg_key, kg_node_keys_by_qg_key, kg, qg, log)
    subclass_self_qedges = {qedge_key: qedge for qedge_key, qedge in qg.edges.items() if _is_subclass_self_qedge(qedge)}
    subclass_qnode_keys = set(subclass_clusters)
    log.debug(f"Subclass self-qedges are {set(subclass_self_qedges)}; qnodes with a subclass self-qedge are: {subclass_qnode_keys}")

    # Return empty result list if have empty KG
    if not kg.nodes:
        log.debug(f"KG is empty - no results.")
        return results

    # Recompute kg_node_keys_by_qg_keys so that it only contains parents (we 'collapse' children into parents)
    kg_node_keys_by_qg_key_collapsed = dict()
    for qnode_key, node_keys in kg_node_keys_by_qg_key.items():
        if qnode_key in subclass_qnode_keys:
            collapsed_node_keys = {child_to_parent_map[qnode_key][node_key] for node_key in node_keys}
            kg_node_keys_by_qg_key_collapsed[qnode_key] = collapsed_node_keys
        else:
            kg_node_keys_by_qg_key_collapsed[qnode_key] = node_keys

    # Handle case where QG contains multiple qnodes and no qedges (we'll dump everything in one result)
    edge_keys_by_node_pair = collections.defaultdict(lambda: collections.defaultdict(lambda: set()))
    if not qg.edges and len(qg.nodes) > 1:
        log.debug(f"QG contains only qnodes (no qedges); will create only one result with all qnodes.")
        result_graph = _create_new_empty_result_graph()
        result_graph["nodes"] = kg_node_keys_by_qg_key_collapsed
        final_result_graphs = [result_graph]
    else:
        # Build up some indexes for edges in the KG (by their subject/object nodes and qedge keys)
        log.debug(f"Building helper indexes for faster lookup of edges")
        edge_keys_by_subject_collapsed = collections.defaultdict(lambda: collections.defaultdict(lambda: set()))
        edge_keys_by_object_collapsed = collections.defaultdict(lambda: collections.defaultdict(lambda: set()))
        edge_keys_by_node_pair_collapsed = collections.defaultdict(lambda: collections.defaultdict(lambda: set()))
        for edge_key, edge in kg.edges.items():
            for qedge_id in edge.qedge_keys:
                qedge = qg.edges[qedge_id]
                # Remap edges to parent concepts for qnodes where subclass answers were provided
                qnode_subj_fulfills, qnode_obj_fulfills = _get_qnodes_subj_and_obj_fulfill(edge, qedge, kg_node_keys_by_qg_key)
                edge_subject = child_to_parent_map[qnode_subj_fulfills][edge.subject] if qnode_subj_fulfills in subclass_qnode_keys else edge.subject
                edge_object = child_to_parent_map[qnode_obj_fulfills][edge.object] if qnode_obj_fulfills in subclass_qnode_keys else edge.object
                edge_keys_by_subject_collapsed[qedge_id][edge_subject].add(edge_key)
                edge_keys_by_object_collapsed[qedge_id][edge_object].add(edge_key)
                edge_keys_by_node_pair_collapsed[qedge_id][(edge_subject, edge_object)].add(edge_key)
                edge_keys_by_node_pair[qedge_id][(edge.subject, edge.object)].add(edge_key)
                if ignore_edge_direction:
                    edge_keys_by_node_pair_collapsed[qedge_id][(edge_object, edge_subject)].add(edge_key)
                    edge_keys_by_node_pair[qedge_id][(edge.object, edge.subject)].add(edge_key)

        # Create results off the "required" portion of the QG (excluding any qnodes/qedges belong to an "option group")
        # Note: Subclass self-qedges are not considered 'required' (they always have an option group specified)
        log.debug(f"Grabbing only required portion of QG")
        required_qg = QueryGraph(nodes={qnode_key: qnode for qnode_key, qnode in qg.nodes.items() if not qnode.option_group_id},
                                 edges={qedge_key: qedge for qedge_key, qedge in qg.edges.items() if not qedge.option_group_id})
        log.debug(f"Required qnodes are {set(required_qg.nodes)}, required qedges are {set(required_qg.edges)}")
        qg_is_disconnected = _qg_is_disconnected(required_qg)
        if qg_is_disconnected:
            log.error(f"Required portion of QG is disconnected. This isn't allowed! 'Required' qnode IDs are: "
                      f"{[qnode_key for qnode_key in required_qg.nodes]}", error_code="DisconnectedQG")
            return []
        # Make sure all required qnodes/qedges are fulfilled
        unfulfilled_qnode_keys = set(required_qg.nodes).difference(kg_node_keys_by_qg_key_collapsed)
        unfulfilled_qedge_keys = set(required_qg.edges).difference(kg_edge_keys_by_qg_key)
        if unfulfilled_qnode_keys or unfulfilled_qedge_keys:
            log.debug(f"KG does not fulfill the (required portion of the) QG. Unfulfilled qnodes: {unfulfilled_qnode_keys}. "
                      f"Unfulfilled qedges: {unfulfilled_qedge_keys}.")
            return results
        log.info(f"Creating result graphs for required portion of QG")
        result_graphs_required = _create_result_graphs(required_qg, kg_node_keys_by_qg_key_collapsed,
                                                       edge_keys_by_subject_collapsed, edge_keys_by_object_collapsed,
                                                       edge_keys_by_node_pair_collapsed,
                                                       ignore_edge_direction, log)
        log.debug(f"Created {len(result_graphs_required)} required result graphs")

        # Then create results for each of the 'option groups' in the QG (including the 'required' portion with each)
        subclass_self_qedge_groups = {qedge.option_group_id for qedge_key, qedge in qg.edges.items()
                                      if _is_subclass_self_qedge(qedge)}
        option_groups_in_qg = {qedge.option_group_id for qedge in qg.edges.values()
                               if qedge.option_group_id}.difference(subclass_self_qedge_groups)
        # NOTE: We ignore subclass self-qedge option groups and instead process those in a different way
        if option_groups_in_qg:
            log.info(f"Distinct option groups detected in the QG are: {option_groups_in_qg}")
        option_group_results_dict = dict()
        for option_group_id in option_groups_in_qg:
            # Include qnodes/qedges that are either required or belong to this option group in our QG for this run
            option_group_qg = QueryGraph(nodes={qnode_key: qnode for qnode_key, qnode in qg.nodes.items()
                                                if qnode.option_group_id == option_group_id or not qnode.option_group_id},
                                         edges={qedge_key: qedge for qedge_key, qedge in qg.edges.items()
                                                if qedge.option_group_id == option_group_id or not qedge.option_group_id})
            log.debug(f"For option group {option_group_id}, qnodes are {set(option_group_qg.nodes)}, "
                      f"qedges are {set(option_group_qg.edges)}")
            qg_is_disconnected = _qg_is_disconnected(option_group_qg)
            if qg_is_disconnected:
                log.error(f"Required + option group {option_group_id} portion of the QG is disconnected. "
                          f"This isn't allowed! 'Required'/group {option_group_id} qnode IDs are: "
                          f"{[qnode_key for qnode_key in option_group_qg.nodes]}", error_code="DisconnectedQG")
                return []
            log.info(f"Creating result graphs for option group {option_group_id}")
            result_graphs_for_option_group = _create_result_graphs(option_group_qg, kg_node_keys_by_qg_key_collapsed,
                                                                   edge_keys_by_subject_collapsed, edge_keys_by_object_collapsed,
                                                                   edge_keys_by_node_pair_collapsed,
                                                                   ignore_edge_direction, log,
                                                                   base_result_graphs=copy.deepcopy(result_graphs_required))
            log.debug(f"Created {len(result_graphs_for_option_group)} option group {option_group_id} result graphs")
            option_group_results_dict[option_group_id] = result_graphs_for_option_group

        # Organize our results for the 'required' portion of the QG by the IDs of their is_set=False nodes
        required_non_set_qnode_keys = [qnode_key for qnode_key, qnode in required_qg.nodes.items() if not qnode.is_set]
        log.debug(f"Required non-set qnodes are: {required_non_set_qnode_keys}")
        result_graphs_by_key = dict()
        for result_graph in result_graphs_required:
            result_key = _get_result_graph_key(result_graph, required_non_set_qnode_keys, log)
            result_graphs_by_key[result_key] = result_graph
        # Then merge our results for each option group ID into the appropriate "required" results
        if option_groups_in_qg:
            log.info(f"Merging option group result graphs into required result graphs with matching non-set qnodes")
        for option_group_id in option_groups_in_qg:
            for option_group_result_graph in option_group_results_dict[option_group_id]:
                result_key = _get_result_graph_key(option_group_result_graph, required_non_set_qnode_keys, log)
                corresponding_result_graph = result_graphs_by_key[result_key]
                # Merge this optional result's contents into its corresponding "required" result
                result_graphs_by_key[result_key] = _merge_optional_into_required_result_graph(option_group_result_graph, corresponding_result_graph)

        final_result_graphs = list(result_graphs_by_key.values())
        log.debug(f"There are a total of {len(final_result_graphs)} final result graphs")

    # ---------------------- Separate children from parents now that results have been formed ------------------ #
    qg_adj_map = _get_qg_adj_map_undirected(qg)
    for result_graph in final_result_graphs:
        # First add original edge subject/object (children) nodes to result and create a helper map for later use
        result_edge_keys_by_node_pair = collections.defaultdict(lambda: collections.defaultdict(lambda: set()))
        for qedge_key, edge_keys in result_graph["edges"].items():
            qedge = qg.edges[qedge_key]
            for edge_key in edge_keys:
                original_edge = kg.edges[edge_key]
                # Don't add nodes arbitrarily because in rare case subj/obj can be indistinguishable...
                if not ((original_edge.subject in result_graph["nodes"][qedge.subject] and original_edge.object in result_graph["nodes"][qedge.object]) or
                        (original_edge.object in result_graph["nodes"][qedge.subject] and original_edge.subject in result_graph["nodes"][qedge.object])):
                    qnode_subj_fulfills, qnode_obj_fulfills = _get_qnodes_subj_and_obj_fulfill(original_edge, qedge, kg_node_keys_by_qg_key)
                    result_graph["nodes"][qnode_subj_fulfills].add(original_edge.subject)
                    result_graph["nodes"][qnode_obj_fulfills].add(original_edge.object)
                # Record this edge under its node pair
                node_pair_key = (original_edge.subject, original_edge.object)
                result_edge_keys_by_node_pair[qedge_key][node_pair_key].add(edge_key)
                if ignore_edge_direction:
                    node_pair_key_other_direction = (original_edge.object, original_edge.subject)
                    result_edge_keys_by_node_pair[qedge_key][node_pair_key_other_direction].add(edge_key)
        # Then go through and remove unhelpful ('dead-end') children and add in the proper subclass self-edges
        result_kg_adj_map = _get_kg_node_adj_map_by_qg_key(result_graph["nodes"], result_edge_keys_by_node_pair, qg)
        for subclass_qnode_key in subclass_qnode_keys:
            subclass_qedge_key = next(qedge_key for qedge_key, qedge in subclass_self_qedges.items()
                                      if qedge.subject == subclass_qnode_key)
            # Remove any child nodes that don't make any connections beyond the parent's connections
            qnode_neighbor_keys = qg_adj_map[subclass_qnode_key].difference({subclass_qnode_key})
            result_node_keys = result_graph["nodes"][subclass_qnode_key]
            node_keys_to_remove = set()
            for result_node_key in result_node_keys:
                parent_key = child_to_parent_map[subclass_qnode_key][result_node_key]
                node_is_child = parent_key != result_node_key
                if node_is_child:  # We'll leave parents in no matter what
                    child_is_useful = _node_in_result_is_useful(result_node_key, subclass_qnode_key, parent_key,
                                                                qnode_neighbor_keys, result_kg_adj_map)
                    if child_is_useful:
                        # Add a subclass self-edge between this child and its parent
                        node_pair_key = (result_node_key, parent_key)
                        subclass_edge_keys = edge_keys_by_node_pair[subclass_qedge_key][node_pair_key]
                        for subclass_edge_key in subclass_edge_keys:
                            result_graph["edges"][subclass_qedge_key].add(subclass_edge_key)
                    else:
                        # Mark this child for removal
                        node_keys_to_remove.add(result_node_key)

            # Actually remove the children we marked for removal
            for node_key_to_remove in node_keys_to_remove:
                result_graph["nodes"][subclass_qnode_key].remove(node_key_to_remove)

        # Remove any edges that were orphaned due to removing children (within this result)
        edges_to_remove = collections.defaultdict(set)
        node_keys_in_result = set.union(*result_graph["nodes"].values())
        for qedge_key, edge_keys in result_graph["edges"].items():
            for edge_key in edge_keys:
                edge = kg.edges[edge_key]
                if not {edge.subject, edge.object}.issubset(node_keys_in_result):
                    edges_to_remove[qedge_key].add(edge_key)
        for qedge_key, edge_keys_to_remove in edges_to_remove.items():
            result_graph["edges"][qedge_key] = result_graph["edges"][qedge_key].difference(edge_keys_to_remove)

    # ------------------ Convert the final result graphs into actual Swagger object model results ----------- #
    log.debug(f"Loading final result graphs into TRAPI object model")
    qnodes_with_multiple_ids = {qnode_key for qnode_key, qnode in qg.nodes.items()
                                if qnode.ids and len(qnode.ids) > 1}
    results = []
    for result_graph in final_result_graphs:
        node_bindings = dict()
        for qnode_key, node_keys in result_graph['nodes'].items():
            node_bindings[qnode_key] = [NodeBinding(id=node_key,
                                                    query_id=_get_query_id(node_key, kg.nodes[node_key], qnode_key,
                                                                           qnodes_with_multiple_ids))
                                        for node_key in node_keys]
        edge_bindings = dict()
        for qedge_key, edge_keys in result_graph['edges'].items():
            edge_bindings[qedge_key] = [EdgeBinding(id=edge_key) for edge_key in edge_keys]
        result = Result(node_bindings=node_bindings, edge_bindings=edge_bindings)

        # Fill out the essence for the result
        essence_qnode_key = _get_essence_node_for_qg(qg)
        essence_qnode = qg.nodes.get(essence_qnode_key)
        essence_kg_node_key_set = result_graph['nodes'].get(essence_qnode_key, set())
        if len(essence_kg_node_key_set) == 0:
            result.essence = cast(str, None)
            result.essence_category = cast(str, None)
        else:
            if len(essence_kg_node_key_set) == 1:
                essence_kg_node_key = next(iter(essence_kg_node_key_set))
            else:
                # Use parent concepts as essence, if available
                if essence_qnode_key in child_to_parent_map:
                    parent_keys = {child_to_parent_map[essence_qnode_key][node_key] for node_key in essence_kg_node_key_set}
                    if len(parent_keys) == 1:
                        essence_kg_node_key = next(iter(parent_keys))
                    elif not essence_qnode.is_set:
                        log.error(f"Result contains more than one 'parent' concept fulfilling the essence qnode "
                                  f"({essence_qnode_key}, which has is_set=False). This shouldn't be possible!",
                                  error_code="SubclassProblem")
                        essence_kg_node_key = next(iter(parent_keys))
                    else:
                        log.warning(f"A result has multiple parent concepts fulfilling the essence qnode {essence_qnode_key}; "
                                    f"will arbitrarily select one to use as the essence for this result")
                        essence_kg_node_key = next(iter(parent_keys))
                # Otherwise we'll have to just pick one KG node to use as essence
                else:
                    log.warning(f"A result has multiple KG nodes fulfilling the essence Qnode {essence_qnode_key}; "
                                f"will arbitrarily select one to use as the essence for this result {result}")
                    essence_kg_node_key = next(iter(essence_kg_node_key_set))

            essence_kg_node = kg.nodes[essence_kg_node_key]
            result.essence = essence_kg_node.name
            if result.essence is None:
                result.essence = essence_kg_node_key
            assert result.essence is not None
            if essence_kg_node.attributes:
                symbol_attributes = [attribute for attribute in essence_kg_node.attributes if attribute.original_attribute_name == "symbol"]
                symbol_attribute = symbol_attributes[0] if symbol_attributes else None
                if symbol_attribute and symbol_attribute.value is not None:
                    result.essence += " (" + str(symbol_attribute.value) + ")"
            result.essence_category = str(essence_qnode.categories) if essence_qnode else None

        # Programmatically generating an informative description for each result
        # seems difficult, but having something non-None is required by the
        # database.  Just put in a placeholder for now, as is done by the
        # QueryGraphReasoner
        result.description = "No description available"  # see issue 642

        results.append(result)

    log.info(f"Resultify created {len(results)} results")
    log.total_results_count = len(results)
    return results


def _node_in_result_is_useful(node_key: str, qnode_key: str, parent_key: str, qnode_neighbor_keys: Set[str],
                              result_kg_adj_map: Dict[str, Dict[str, Dict[str, Set[str]]]]) -> bool:
    """
    This function determines whether the given node (node_key, which fulfills qnode_key), adds information to a result
    beyond that provided by its parent (parent_key). Nodes that do not can be considered 'dead ends'. Dead end
    child nodes are somewhat similar to dead-end intermediate nodes when is_set=True; if they don't link through on a
    path that matches the full QG, they're dead ends. The difference is that together a parent + child need to link
    through, instead of a single node.
    """
    has_same_neighbors = []
    has_additional_neighbors = []
    for qnode_neighbor_key in qnode_neighbor_keys:
        parent_neighbors = result_kg_adj_map[qnode_key][parent_key][qnode_neighbor_key]
        own_neighbors = result_kg_adj_map[qnode_key][node_key][qnode_neighbor_key]
        if own_neighbors.difference(parent_neighbors):
            has_additional_neighbors.append(True)
        else:
            has_additional_neighbors.append(False)
        if own_neighbors == parent_neighbors:
            has_same_neighbors.append(True)
        else:
            has_same_neighbors.append(False)
    return True if any(has_additional_neighbors) or all(has_same_neighbors) else False


def _get_result_graph_counts(result_graph):
    qnode_counts = {qnode_key: len(nodes) for qnode_key, nodes in result_graph["nodes"].items()}
    qedge_counts = {qedge_key: len(edges) for qedge_key, edges in result_graph["edges"].items()}
    return f"nodes {qnode_counts}, edges {qedge_counts}"


def _qg_is_disconnected(qg: QueryGraph) -> bool:
    qnode_keys_examined = {next(qnode_key for qnode_key in qg.nodes)} if qg.nodes else set()  # Start with any qnode
    qnode_keys_remaining = set(qg.nodes).difference(qnode_keys_examined)
    # Repeatedly look for a qnode connected to at least one of the already examined qnodes
    connected_qnode_key, _ = _find_qnode_connected_to_sub_qg(qnode_keys_examined, qnode_keys_remaining, qg)
    while connected_qnode_key and qnode_keys_remaining:
        qnode_keys_remaining.remove(connected_qnode_key)
        qnode_keys_examined.add(connected_qnode_key)
        connected_qnode_key, _ = _find_qnode_connected_to_sub_qg(qnode_keys_examined, qnode_keys_remaining, qg)
    # The QG must be disconnected if there are qnodes remaining that are not connected to any of our examined ones
    return True if not connected_qnode_key and qnode_keys_remaining else False


def _merge_optional_into_required_result_graph(optional_result_graph: Dict[str, Dict[str, Set[str]]],
                                               required_result_graph: Dict[str, Dict[str, Set[str]]]) -> Dict[str, Dict[str, Set[str]]]:
    # Start with the required result graph and then add in any nodes/edges from the optional graph as appropriate
    merged_result_graph = _copy_result_graph(required_result_graph)
    for qnode_key, optional_kg_node_keys in optional_result_graph["nodes"].items():
        if qnode_key in required_result_graph["nodes"]:
            merged_result_graph["nodes"][qnode_key] = required_result_graph["nodes"][qnode_key].union(optional_kg_node_keys)
        else:
            merged_result_graph["nodes"][qnode_key] = optional_kg_node_keys
    for qedge_key, optional_kg_edge_keys in optional_result_graph["edges"].items():
        if qedge_key in required_result_graph["edges"]:
            merged_result_graph["edges"][qedge_key] = required_result_graph["edges"][qedge_key].union(optional_kg_edge_keys)
        else:
            merged_result_graph["edges"][qedge_key] = optional_kg_edge_keys
    return merged_result_graph


def _get_qnodes_subj_and_obj_fulfill(edge: Edge, qedge: QEdge, kg_node_keys_by_qg_key: Dict[str, Set[str]]) -> Tuple[str, str]:
    if edge.subject in kg_node_keys_by_qg_key[qedge.subject] and edge.object in kg_node_keys_by_qg_key[qedge.object]:
        return qedge.subject, qedge.object
    else:
        return qedge.object, qedge.subject


def _get_result_graph_key(result_graph: Dict[str, Dict[str, Set[str]]], qnodes_to_merge_on: List[str],
                          log: ARAXResponse) -> str:
    """
    Result graph keys are defined such that result graphs with the same key should be merged. The overall result graph
    key is a concatenation of one 'merge key' per each qnode to merge on, which generally should be each is_set=False,
    required qnode. Each result should only have one node fulfilling a required, is_set=False qnode, so the ID for
    that node is used as the merge key. Note that for subclass/query_id handling, we map children to parents BEFORE
    resultifying, and undo that mapping afterwards, which means we don't need a special case here to handle that.
    (Merge keys are automatically parents for such subclass qnodes because of this pre-mapping that we do.)
    """
    qnodes_to_merge_on.sort()
    merge_keys = []
    for qnode_key in qnodes_to_merge_on:
        result_nodes = list(result_graph["nodes"][qnode_key])
        if len(result_nodes) > 1:
            log.error(f"Do not know what merge key to use when more than one node is fulfilling a non-set"
                      f" qnode. Nodes {result_nodes} are fulfilling {qnode_key} in a single result. "
                      f"Result graph is {result_graph}", error_code="MergeKeyError")
            merge_key = "?"
        else:
            # There must only be one node fulfilling this qnode
            merge_key = result_nodes[0]
        merge_keys.append(merge_key)
    result_graph_key = "--".join(merge_keys)
    return result_graph_key


def _get_kg_node_keys_by_qg_key(knowledge_graph: KnowledgeGraph) -> Dict[str, Set[str]]:
    node_keys_by_qg_key = dict()
    for node_key, node in knowledge_graph.nodes.items():
        if node.qnode_keys:
            for qnode_key in node.qnode_keys:
                if qnode_key not in node_keys_by_qg_key:
                    node_keys_by_qg_key[qnode_key] = set()
                node_keys_by_qg_key[qnode_key].add(node_key)
    return node_keys_by_qg_key


def _get_kg_edge_keys_by_qg_key(knowledge_graph: KnowledgeGraph) -> Dict[str, Set[str]]:
    edge_keys_by_qg_key = dict()
    for edge_key, edge in knowledge_graph.edges.items():
        if edge.qedge_keys:
            for qedge_key in edge.qedge_keys:
                if qedge_key not in edge_keys_by_qg_key:
                    edge_keys_by_qg_key[qedge_key] = set()
                edge_keys_by_qg_key[qedge_key].add(edge_key)
    return edge_keys_by_qg_key


def _get_connected_qnode_keys(qnode_key: str, query_graph: QueryGraph) -> Set[str]:
    qnode_keys_used_on_same_qedges = set()
    for qedge in query_graph.edges.values():
        if qnode_key in {qedge.subject, qedge.object}:
            qnode_keys_used_on_same_qedges.add(qedge.subject)
            qnode_keys_used_on_same_qedges.add(qedge.object)
    return qnode_keys_used_on_same_qedges.difference({qnode_key})


def _create_new_empty_result_graph() -> Dict[str, Dict[str, Set[str]]]:
    empty_result_graph = {'nodes': collections.defaultdict(set),
                          'edges': collections.defaultdict(set),
                          'parents': collections.defaultdict(str)}
    return empty_result_graph


def _copy_result_graph(result_graph: Dict[str, Dict[str, Set[str]]]) -> Dict[str, Dict[str, Set[str]]]:
    result_graph_copy = copy.deepcopy(result_graph)
    return result_graph_copy


def _get_parallel_qedge_keys(input_qedge: QEdge, query_graph: QueryGraph) -> Set[str]:
    input_qedge_node_keys = {input_qedge.subject, input_qedge.object}
    parallel_qedge_keys = {qedge_key for qedge_key, qedge in query_graph.edges.items() if {qedge.subject, qedge.object} == input_qedge_node_keys}
    return parallel_qedge_keys


def _get_query_id(node_key: str, node: Node, qnode_key: str, qnode_keys_with_multiple_ids: Set[str]) -> Optional[str]:
    # TODO: Should this really be looking at child to parent map, instead of node's query_ids?
    if qnode_key in qnode_keys_with_multiple_ids:
        if hasattr(node, "query_ids") and node.query_ids:
            query_id = _get_best_parent_id(node.query_ids)  # TODO: How should multiple query_ids be handled?? Separate results? Brought up in #1871
            return query_id if query_id != node_key else None
        else:
            return None
    else:
        return None


def _get_kg_node_adj_map_by_qg_key(kg_node_keys_by_qg_key: Dict[str, Set[str]],
                                   edge_keys_by_node_pair: DefaultDict[str, DefaultDict[Tuple[str], set]],
                                   qg: QueryGraph) -> Dict[str, Dict[str, Dict[str, Set[str]]]]:
    # Returned dict looks like {'n00': {'UMLS:11234': {'n01': {UniProtKB:122}}}}
    # First initiate the overall structure of our (QG-organized) adjacency map
    kg_node_to_node_map = {qnode_key: dict() for qnode_key in kg_node_keys_by_qg_key}
    for qnode_key in qg.nodes:
        node_keys_set = kg_node_keys_by_qg_key[qnode_key]
        connected_qnode_keys = _get_connected_qnode_keys(qnode_key, qg)
        for node_key in node_keys_set:
            kg_node_to_node_map[qnode_key][node_key] = {connected_qnode_key: set() for connected_qnode_key in connected_qnode_keys}

    # Create a record of which qedge IDs are fulfilled between which node pairs
    node_pair_to_qedge_key_map = collections.defaultdict(set)
    for qedge_key, node_pairs_dict in edge_keys_by_node_pair.items():
        for node_pair_key in node_pairs_dict:
            node_pair_to_qedge_key_map[node_pair_key].add(qedge_key)

    # Fill out which KG nodes are connected to which
    for qedge_key, node_pairs_dict in edge_keys_by_node_pair.items():
        # Note: KG may contain some qedge IDs not in this version of the QG due to option group handling
        if qedge_key in qg.edges:
            qedge = qg.edges[qedge_key]
            # Make sure ALL qedges between this edge's two nodes have been fulfilled before marking them as 'connected'
            parallel_qedge_keys = _get_parallel_qedge_keys(qedge, qg)
            for node_pair_key in node_pairs_dict:
                if parallel_qedge_keys.issubset(node_pair_to_qedge_key_map[node_pair_key]):
                    node_a = node_pair_key[0]
                    node_b = node_pair_key[1]
                    qnode_key_1 = qedge.subject
                    qnode_key_2 = qedge.object
                    if node_a in kg_node_keys_by_qg_key[qnode_key_1] and node_b in kg_node_keys_by_qg_key[qnode_key_2]:
                        kg_node_to_node_map[qnode_key_1][node_a][qnode_key_2].add(node_b)
                        kg_node_to_node_map[qnode_key_2][node_b][qnode_key_1].add(node_a)
                    if node_a in kg_node_keys_by_qg_key[qnode_key_2] and node_b in kg_node_keys_by_qg_key[qnode_key_1]:
                        kg_node_to_node_map[qnode_key_2][node_a][qnode_key_1].add(node_b)
                        kg_node_to_node_map[qnode_key_1][node_b][qnode_key_2].add(node_a)

    return kg_node_to_node_map


def _result_graph_is_fulfilled(result_graph: Dict[str, Dict[str, Set[str]]], query_graph: QueryGraph,
                               nodes_only: bool = False) -> bool:
    for qnode_key in query_graph.nodes:
        if not result_graph['nodes'].get(qnode_key):
            return False
    if not nodes_only:
        for qedge_key in query_graph.edges:
            if not result_graph['edges'].get(qedge_key):
                return False
    return True


def _is_subclass_self_qedge(qedge: QEdge) -> bool:
    return qedge.subject == qedge.object and qedge.predicates == ["biolink:subclass_of"]


def _get_all_adjacent_nodes(kg_node_keys: Set[str], start_qnode_key: str, target_qnode_key: str,
                            kg_node_adj_map_by_qg_key: Dict[str, Dict[str, Dict[str, Set[str]]]]) -> Set[str]:
    """
    This function returns all nodes adjacent to a set of nodes (kg_node_keys) that fulfill the target_qnode_key. The
    start_qnode_key is the qnode ID that the set of input nodes fulfill. Being adjacent to the set of nodes means that
    the node is connected to ANY of the individual input nodes in the set (not ALL).
    """
    connections = [kg_node_adj_map_by_qg_key[start_qnode_key][kg_node_key][target_qnode_key] for kg_node_key in kg_node_keys]
    return {node_key for node_key_set in connections for node_key in node_key_set}


def _find_qnode_connected_to_sub_qg(qnode_keys_to_connect_to: Set[str], qnode_keys_to_choose_from: Set[str],
                                    qg: QueryGraph) -> Tuple[str, Set[str]]:
    """
    This function selects a qnode ID from the qnode_keys_to_choose_from that connects to one or more of the qnode IDs
    in the qnode_keys_to_connect_to (which is generally a sub-graph of the QG). It also returns the IDs
    of the connection points (all qnode ID(s) in qnode_keys_to_connect_to that the chosen node connects to).
    """
    for qnode_key_option in qnode_keys_to_choose_from:
        all_qedges_using_qnode = [qedge for qedge in qg.edges.values() if qnode_key_option in {qedge.subject, qedge.object}]
        all_connected_qnode_keys = {qnode_key for qedge in all_qedges_using_qnode for qnode_key in
                                    {qedge.subject, qedge.object}}.difference({qnode_key_option})
        subgraph_connections = qnode_keys_to_connect_to.intersection(all_connected_qnode_keys)
        if subgraph_connections:
            return qnode_key_option, subgraph_connections
    return "", set()


def _get_qg_adj_map_undirected(qg: QueryGraph) -> Dict[str, Set[str]]:
    """
    This function creates a node adjacency map for a given query graph. Example: {"n0": {"n1"}, "n1": {"n0"}}
    """
    qg_adj_map = dict()
    for qnode_key in qg.nodes:
        connected_qedges = [qedge for qedge in qg.edges.values() if qnode_key in {qedge.subject, qedge.object}]
        connected_qnode_keys = set()
        for connected_qedge in connected_qedges:
            # Note: The below purposefully allows other_qnode_key to be equal to qnode_key for self-qedges only
            other_qnode_key = connected_qedge.subject if connected_qedge.subject != qnode_key else connected_qedge.object
            connected_qnode_keys.add(other_qnode_key)
        qg_adj_map[qnode_key] = connected_qnode_keys
    return qg_adj_map


def _extract_sub_qg_adj_map(qg_adj_map: Dict[str, Set[str]], allowed_qnode_keys: Set[str]) -> Dict[str, Set[str]]:
    """
    This function extracts the node adjacency info for a "subgraph" of the query graph (represented by
    allowed_qnode_keys). Example of qg_adj_map: {"n0": {"n1"}, "n1": {"n0"}}
    """
    return {qnode_key: neighbor_qnode_keys.intersection(allowed_qnode_keys)
            for qnode_key, neighbor_qnode_keys in qg_adj_map.items() if qnode_key in allowed_qnode_keys}


def _get_subclass_clusters(kg_edge_keys_by_qg_key: Dict[str, Set[str]], kg_node_keys_by_qg_key: Dict[str, Set[str]],
                           kg: KnowledgeGraph, qg: QueryGraph,
                           log: ARAXResponse) -> Tuple[Dict[str, Dict[str, Set[str]]], Dict[str, Dict[str, str]]]:
    """
    Create two helper maps: one that maps parent IDs to their set of child IDs, and another that maps child IDs to
    their parent IDs. Parent-child relationships are determined based on the edges in the KG fulfilling any subclass
    self-qedges that Expand added to the QG.
    """
    subclass_self_qedges = {qedge_key for qedge_key, qedge in qg.edges.items()
                            if _is_subclass_self_qedge(qedge)}
    subclass_self_qnodes = {qg.edges[qedge_key].subject for qedge_key in subclass_self_qedges}
    subclass_clusters = {subclass_qnode_key: dict() for subclass_qnode_key in subclass_self_qnodes}
    child_to_parents_map = {subclass_qnode_key: collections.defaultdict(set) for subclass_qnode_key in subclass_self_qnodes}
    # Record parent/child mappings based on the subclass self-edges in the KG
    for subclass_qedge_key in subclass_self_qedges:
        subclass_qnode_key = qg.edges[subclass_qedge_key].subject
        for edge_key in kg_edge_keys_by_qg_key.get(subclass_qedge_key, set()):
            edge = kg.edges[edge_key]
            parent_id = edge.object
            child_id = edge.subject
            if parent_id not in subclass_clusters[subclass_qnode_key]:
                subclass_clusters[subclass_qnode_key][parent_id] = {parent_id}
            subclass_clusters[subclass_qnode_key][parent_id].add(child_id)
            child_to_parents_map[subclass_qnode_key][child_id].add(parent_id)
            child_to_parents_map[subclass_qnode_key][parent_id].add(parent_id)  # It's handy to list parents as parent of themselves
    # Also list any parents that may not have subclass self-edges as parents of themselves
    for subclass_qnode_key in subclass_self_qnodes:
        node_keys_for_qnode = kg_node_keys_by_qg_key[subclass_qnode_key]
        node_keys_with_subclass_edge = set(child_to_parents_map[subclass_qnode_key])
        node_keys_with_no_subclass_edge = node_keys_for_qnode.difference(node_keys_with_subclass_edge)
        for node_key in node_keys_with_no_subclass_edge:
            # Otherwise we'll record this node as its own parent
            # Note: Such nodes won't always appear in the QG because they can be returned for unpinned nodes
            child_to_parents_map[subclass_qnode_key][node_key] = {node_key}

    # Keep only one parent per child per qnode key; system/TRAPI isn't set up to handle multiple yet
    child_to_parent_map = {subclass_qnode_key: dict() for subclass_qnode_key in subclass_self_qnodes}
    for qnode_key, mappings in child_to_parents_map.items():
        for child_id, parent_ids in mappings.items():
            best_parent_id = _get_best_parent_id(parent_ids)
            child_to_parent_map[qnode_key][child_id] = best_parent_id

    return subclass_clusters, child_to_parent_map


def _get_best_parent_id(parent_ids: Iterable[str]) -> str:
    # TODO: TRAPI and our system elsewhere isn't set up to handle multiple query IDs per node... change eventually?
    return list(sorted(list(parent_ids)))[0]


def _clean_up_dead_ends(result_graph: Dict[str, Dict[str, Set[str]]],
                        sub_qg_adj_map: Dict[str, Set[str]],
                        kg_node_adj_map_by_qg_key: Dict[str, Dict[str, Dict[str, Set[str]]]],
                        log: ARAXResponse) -> Dict[str, Dict[str, Set[str]]]:
    """
    This function iteratively removes "dead ends" from a result graph until no more dead ends can be found. Dead ends
    can be thought of as intermediate nodes (typically for is_set=True qnodes) that connect to only a subset of the
    nodes they should be connected to according to the query graph. Only the part of the result graph that has been
    "fulfilled" so far during the result construction process is evaluated here: the sub_qg_adj_map must contain only
    info for qnodes fulfilled thus far.
    """
    fulfilled_qnode_keys = set(sub_qg_adj_map)
    found_dead_ends = True
    while found_dead_ends:
        found_dead_ends = False
        nodes_to_remove = dict()
        # Go through each qnode "role" in our result graph, and check the nodes corresponding to that qnode
        for qnode_key in fulfilled_qnode_keys:
            corresponding_node_keys = result_graph["nodes"][qnode_key]
            required_neighbor_qnode_keys = sub_qg_adj_map[qnode_key]
            # Make sure each node for this qnode ID is connected to at LEAST one node fulfilling each neighbor qnode ID
            for corresponding_node_key in corresponding_node_keys:
                for neighbor_qnode_key in required_neighbor_qnode_keys:
                    # Look for at least one node in the result graph in this neighbor spot that this node is linked to
                    neighbors_in_kg = kg_node_adj_map_by_qg_key[qnode_key][corresponding_node_key][neighbor_qnode_key]
                    if not neighbors_in_kg.intersection(result_graph["nodes"][neighbor_qnode_key]):
                        # Mark this node for removal from this result graph since it's lacking a neighbor here
                        found_dead_ends = True
                        if qnode_key not in nodes_to_remove:
                            nodes_to_remove[qnode_key] = set()
                        nodes_to_remove[qnode_key].add(corresponding_node_key)
        # Actually go through and remove our nodes marked for removal
        for qnode_key, node_keys in nodes_to_remove.items():
            result_graph["nodes"][qnode_key] = result_graph["nodes"][qnode_key].difference(node_keys)
    return result_graph


def _create_result_graphs(qg: QueryGraph,
                          kg_node_keys_by_qg_key: Dict[str, Set[str]],
                          edge_keys_by_subject: DefaultDict[str, DefaultDict[str, set]],
                          edge_keys_by_object: DefaultDict[str, DefaultDict[str, set]],
                          edge_keys_by_node_pair: DefaultDict[str, DefaultDict[Tuple[str], set]],
                          ignore_edge_direction: bool = True,
                          log: ARAXResponse = ARAXResponse(),
                          base_result_graphs: Optional[List[dict]] = None) -> List[dict]:
    kg_node_adj_map_by_qg_key = _get_kg_node_adj_map_by_qg_key(kg_node_keys_by_qg_key, edge_keys_by_node_pair, qg)
    qg_adj_map = _get_qg_adj_map_undirected(qg)

    # Iteratively construct "result graphs" (initially containing only nodes, not edges) by walking through all qnodes
    log.debug(f"Constructing result graphs qnode by qnode")
    if base_result_graphs:
        # We'll build off of the 'base' result graphs rather than start anew (saves time for option group processing)
        result_graphs = base_result_graphs
        qnode_keys_already_handled = set(result_graphs[0]["nodes"])
        qnode_keys_remaining = set(qg.nodes).difference(qnode_keys_already_handled)
    else:
        result_graphs = []
        qnode_keys_already_handled = set()
        qnode_keys_remaining = set(qg.nodes)
    log.debug(f"Qnode keys already handled are: {qnode_keys_already_handled}")
    while qnode_keys_remaining:
        # Start with a random qnode if this is our first iteration
        if not qnode_keys_already_handled:
            current_qnode_key = list(qnode_keys_remaining)[0]
            prior_qnode_connections = set()
        # Otherwise find a yet unhandled qnode ID that connects somehow to the part of the QG we've already handled
        else:
            current_qnode_key, prior_qnode_connections = _find_qnode_connected_to_sub_qg(qnode_keys_already_handled, qnode_keys_remaining, qg)
            log.debug(f"Next qnode chosen is: {current_qnode_key}")
        current_qnode = qg.nodes[current_qnode_key]

        # Initialize our result graphs if this is our first iteration
        if not result_graphs:
            log.debug(f"Initiating result graphs with nodes for {current_qnode_key} (is_set={current_qnode.is_set})")
            all_node_keys_in_kg_for_this_qnode_key = kg_node_keys_by_qg_key.get(current_qnode_key)
            # We'll start with one result graph with ALL corresponding nodes in the KG in this spot if is_set=True
            if current_qnode.is_set:
                log.debug(f"Starting with one result graph because is_set=True for {current_qnode_key}")
                new_result_graph = _create_new_empty_result_graph()
                new_result_graph["nodes"][current_qnode_key] = all_node_keys_in_kg_for_this_qnode_key  # Parents included already
                result_graphs.append(new_result_graph)
            # Otherwise, we'll start with a result graph for EACH corresponding node in the KG
            else:
                log.debug(f'Starting with a result graph for each {current_qnode_key} node')
                for node_key in all_node_keys_in_kg_for_this_qnode_key:
                    new_result_graph = _create_new_empty_result_graph()
                    new_result_graph["nodes"][current_qnode_key] = {node_key}
                    result_graphs.append(new_result_graph)
        # Otherwise fan out our existing result graphs, filling out this qnode spot in them based on prior contents
        else:
            log.debug(f"Adding a layer to each result graph for qnode {current_qnode_key} (is_set={current_qnode.is_set})")
            new_result_graphs = []
            sub_qg_adj_map = _extract_sub_qg_adj_map(qg_adj_map, qnode_keys_already_handled.union({current_qnode_key}))
            for result_graph in result_graphs:
                # Figure out which KG nodes could fulfill the current qnode in this result
                prior_qnodes_kg_nodes = {prior_qnode_key: result_graph["nodes"][prior_qnode_key] for prior_qnode_key in prior_qnode_connections}
                current_kg_node_possibilities = [_get_all_adjacent_nodes(corresponding_kg_nodes, prior_qnode_key, current_qnode_key, kg_node_adj_map_by_qg_key)
                                                 for prior_qnode_key, corresponding_kg_nodes in prior_qnodes_kg_nodes.items()]
                # Only keep connections that have links to KG nodes in ALL prior connected qnode roles
                final_connected_kg_nodes = set.intersection(*current_kg_node_possibilities)

                # Fan out or add to result graphs as appropriate
                if final_connected_kg_nodes:
                    if current_qnode.is_set:
                        # Replace this result graph with a new one with all valid connections listed under this qnode
                        new_result_graph = _copy_result_graph(result_graph)
                        new_result_graph["nodes"][current_qnode_key] = final_connected_kg_nodes
                        pruned_result_graph = _clean_up_dead_ends(result_graph=new_result_graph,
                                                                  sub_qg_adj_map=sub_qg_adj_map,
                                                                  kg_node_adj_map_by_qg_key=kg_node_adj_map_by_qg_key,
                                                                  log=log)
                        new_result_graphs.append(pruned_result_graph)
                    else:
                        # Create a new result graph for each new valid connected node
                        for connected_node_key in final_connected_kg_nodes:
                            new_result_graph = _copy_result_graph(result_graph)
                            new_result_graph["nodes"][current_qnode_key] = {connected_node_key}
                            pruned_result_graph = _clean_up_dead_ends(result_graph=new_result_graph,
                                                                      sub_qg_adj_map=sub_qg_adj_map,
                                                                      kg_node_adj_map_by_qg_key=kg_node_adj_map_by_qg_key,
                                                                      log=log)
                            new_result_graphs.append(pruned_result_graph)

            result_graphs = new_result_graphs
        log.debug(f"Current count of result graphs is {len(result_graphs)}")

        # Update our records about which qnodes we've already processed
        qnode_keys_remaining.remove(current_qnode_key)
        qnode_keys_already_handled.add(current_qnode_key)
    log.debug(f"Done assigning nodes to result graphs.")

    # Then add edges to our result graphs as appropriate (note, we DON'T add subclass self-qedges for now)
    log.debug(f"Adding edges to result graphs")
    for result_graph in result_graphs:
        for qedge_key, qedge in qg.edges.items():
            qedge_source_node_ids = result_graph['nodes'][qedge.subject]
            qedge_target_node_ids = result_graph['nodes'][qedge.object]
            # Pick the more efficient method for edge-finding depending on the number of nodes for this result/qedge
            if len(qedge_source_node_ids) < 10 or len(qedge_target_node_ids) < 10:
                possible_node_pairs = {(node_1, node_2) for node_1 in qedge_source_node_ids
                                       for node_2 in qedge_target_node_ids}
                for node_pair in possible_node_pairs:
                    ids_of_matching_edges = edge_keys_by_node_pair[qedge_key].get(node_pair, set())
                    result_graph['edges'][qedge_key].update(ids_of_matching_edges)
            else:
                # This technique is more efficient when there are large numbers of both subject and object nodes
                edges_with_matching_subject = {edge_key for source_node in qedge_source_node_ids
                                               for edge_key in edge_keys_by_subject[qedge_key][source_node]}
                edges_with_matching_object = {edge_key for target_node in qedge_target_node_ids
                                              for edge_key in edge_keys_by_object[qedge_key][target_node]}
                result_graph['edges'][qedge_key] = edges_with_matching_subject.intersection(edges_with_matching_object)
                if ignore_edge_direction:
                    edges_with_reverse_subject = {edge_key for target_node in qedge_target_node_ids
                                                  for edge_key in edge_keys_by_subject[qedge_key][target_node]}
                    edges_with_reverse_object = {edge_key for source_node in qedge_source_node_ids
                                                 for edge_key in edge_keys_by_object[qedge_key][source_node]}
                    result_graph['edges'][qedge_key].update(edges_with_reverse_subject.intersection(edges_with_reverse_object))

    # Filter out any results for which not every qnode/qedge is fulfilled (with the exception of subclass self-qedges)
    final_result_graphs = [result_graph for result_graph in result_graphs if _result_graph_is_fulfilled(result_graph, qg)]
    log.debug(f"After pruning out result graphs missing edges, there are {len(final_result_graphs)} result graphs")
    return final_result_graphs


# code/ARAX/ARAXQuery/actions_parser.py

#!/usr/bin/python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast
import re

from ARAX_response import ARAXResponse


class ActionsParser:

    #### Constructor
    def __init__(self):
        pass

    #### Parse the provided action list with validation
    def parse(self, input_actions):

        #### Define a default response
        response = ARAXResponse()
        response.info(f"Parsing input actions list")

        #### Basic error checking of the input_actions
        if not isinstance(input_actions, list):
            response.error("Provided input actions is not a list", error_code="ActionsNotList")
            return response
        if len(input_actions) == 0:
            response.error("Provided input actions is an empty list", error_code="ActionsListEmpty")
            return response

        #### Iterate through the list, checking the items
        actions = []
        n_lines = 1
        for action in input_actions:
            response.debug(f"Parsing action: {action}")

            # If this line is empty, then skip
            match = re.match(r"\s*$",action)
            if match:
                continue

            # If this line begins with a #, it is a comment, then skip
            match = re.match(r"#",action)
            if match:
                continue

            #### First look for a naked command without parentheses
            match = re.match(r"\s*([A-Za-z_]+)\s*$",action)
            if match is not None:
                action = { "line": n_lines, "command": match.group(1), "parameters": None }
                actions.append(action)

            #### Then look for and parse a command with parentheses and a comma-separated parameter list
            if match is None:
                match = re.match(r"\s*([A-Za-z_]+)\((.*)\)\s*$",action)
                if match is not None:
                    command = match.group(1)
                    param_string = match.group(2)

                    #### Split the parameters on comma and process those
                    param_string_list = re.split(",",param_string)
                    parameters = {}

                    #### If a value is of the form key=[value1,value2] special code is needed to recompose that
                    mode = 'normal'
                    list_buffer = []
                    key = ''
                    for param_item in param_string_list:
                        param_item = param_item.strip()
                        if mode == 'normal':

                            #### Split on the first = only (might be = in the value)
                            values = re.split("=",param_item,1)
                            key = values[0]
                            #### If there isn't a value after an =, then just set to string true
                            value = 'true'
                            if len(values) > 1:
                                value = values[1]
                            key = key.strip()
                            value = value.strip()

                            #### If the value begins with a "[", then this is a list
                            match = re.match(r"\[(.+)$",value)
                            if match:
                                #### If it also ends with a "]", then this is a list of one element
                                match2 = re.match(r"\[(.*)\]$",value)
                                if match2:
                                    if match2.group(1) == '':
                                        parameters[key] = [ ]
                                    else:
                                        parameters[key] = [ match2.group(1) ]
                                else:
                                    mode = 'in_list'
                                    list_buffer = [ match.group(1) ]
                            else:
                                parameters[key] = value

                        #### Special processing if we're in the middle of a list
                        elif mode == 'in_list':
                            match = re.match(r"(.*)\]$",param_item)
                            if match:
                                mode = 'normal'
                                list_buffer.append(match.group(1))
                                parameters[key] = list_buffer
                            else:
                                list_buffer.append(param_item)
                        else:
                            eprint("Inconceivable!")
                    if mode == 'in_list':
                        parameters[key] = list_buffer

                    #### Store the parsed result in a dict and add to the list
                    action = { "line": n_lines, "command": command, "parameters": parameters }
                    actions.append(action)
                else:
                    response.error(f"Unable to parse action {action}", error_code="ActionsListEmpty")
            n_lines += 1

        #### Put the actions in the response data envelope and return
        response.data["actions"] = actions
        return response


##########################################################################################
def main():

    #### Create an ActionsParser object
    actions_parser = ActionsParser()
 
    #### Set a simple list of actions
    actions_list = [
        "clear_results",
        "evaluate_query_graph",
        "filter(start_node=1, maximum_results=10, minimum_probability=0.5, maximum_ngd=0.8)",
        "expand(command=expand_nodes, edge_id=[e00,e01,e02], add_virtual_nodes=true)",
        "expand(edge_id=[e00,e01,e02], sort_by=[type,name])",
        "expand(edge_id=[e00], sort_by=[type], empty_list=[] , dangling_comma=[p,q,])",
        "overlay(compute_ngd=true,default)",
        "overlay(add_pubmed_ids=true, start_node=2)",
        "return(message=true,store=false,test=accept=true)"
    ]

    #### Parse the action_list
    result = actions_parser.parse(actions_list)

    #### Print the response information (including debug information)
    print(result.show(level=ARAXResponse.DEBUG))

    #### Print the final response data envelope
    print(json.dumps(ast.literal_eval(repr(result.data)),sort_keys=True,indent=2))

if __name__ == "__main__": main()


# code/ARAX/ARAXQuery/knowledge_graph_info.py

#!/bin/env python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast
import re

from ARAX_response import ARAXResponse
from query_graph_info import QueryGraphInfo


class KnowledgeGraphInfo:

    #### Constructor
    def __init__(self):
        self.response = None
        self.message = None

        self.n_nodes = None
        self.n_edges = None
        self.query_graph_id_node_status = None
        self.query_graph_id_edge_status = None

        self.node_map = {}
        self.edge_map = {}


    #### Top level decision maker for applying filters
    def check_for_query_graph_tags(self, message, query_graph_info):

        #### Define a default response
        response = ARAXResponse()
        self.response = response
        self.message = message
        response.debug(f"Checking KnowledgeGraph for QueryGraph tags")

        #### Get shorter handles
        knowledge_graph = message.knowledge_graph
        nodes = knowledge_graph.nodes
        edges = knowledge_graph.edges

        #### Store number of nodes and edges
        self.n_nodes = len(nodes)
        self.n_edges = len(edges)
        response.debug(f"Found {self.n_nodes} nodes and {self.n_edges} edges")

        #### Clear the maps
        self.node_map = { 'by_qnode_id': {} }
        self.edge_map = { 'by_qedge_id': {} }

        #### Loop through nodes computing some stats
        n_nodes_with_query_graph_ids = 0
        for key,node in nodes.items():
            if node.qnode_id is None:
                continue
            n_nodes_with_query_graph_ids += 1

            #### Place an entry in the node_map
            if node.qnode_id not in self.node_map['by_qnode_id']:
                self.node_map['by_qnode_id'][node.qnode_id] = {}
            self.node_map['by_qnode_id'][node.qnode_id][key] = 1

        #### Tally the stats
        if n_nodes_with_query_graph_ids == self.n_nodes:
            self.query_graph_id_node_status = 'all nodes have query_graph_ids'
        elif n_nodes_with_query_graph_ids == 0:
            self.query_graph_id_node_status = 'no nodes have query_graph_ids'
        else:
            self.query_graph_id_node_status = 'only some nodes have query_graph_ids'
        response.info(f"In the KnowledgeGraph, {self.query_graph_id_node_status}")

        #### Loop through edges computing some stats
        n_edges_with_query_graph_ids = 0
        for key,edge in edges.items():
            if edge.qedge_id is None:
                continue
            n_edges_with_query_graph_ids += 1

            #### Place an entry in the edge_map
            if edge.qedge_id not in self.edge_map['by_qedge_id']:
                self.edge_map['by_qedge_id'][edge.qedge_id] = {}
            self.edge_map['by_qedge_id'][edge.qedge_id][key] = 1

        if n_edges_with_query_graph_ids == self.n_edges:
            self.query_graph_id_edge_status = 'all edges have query_graph_ids'
        elif n_edges_with_query_graph_ids == 0:
            self.query_graph_id_edge_status = 'no edges have query_graph_ids'
        else:
            self.query_graph_id_edge_status = 'only some edges have query_graph_ids'
        response.info(f"In the KnowledgeGraph, {self.query_graph_id_edge_status}")

        #### Return the response
        return response



    #### Top level decision maker for applying filters
    def add_query_graph_tags(self, message, query_graph_info):

        #### Define a default response
        response = ARAXResponse()
        self.response = response
        self.message = message
        response.debug(f"Adding temporary QueryGraph ids to KnowledgeGraph")

        #### Get shorter handles
        knowedge_graph = message.knowledge_graph
        nodes = knowedge_graph.nodes
        edges = knowedge_graph.edges

        #### Loop through nodes adding qnode_ids
        for key,node in nodes.items():

            #### If there is not qnode_id, then determine what it should be and add it
            if node.qnode_id is None:
                categorys = node.category
 
                #### Find a matching category in the QueryGraph for this node
                if categorys is None:
                    response.error(f"KnowledgeGraph node {key} does not have a category. This should never be", error_code="NodeMissingCategory")
                    return response
                n_found_categorys = 0
                found_category = None
                for node_category in categorys:
                    if node_category in query_graph_info.node_category_map:
                        n_found_categorys += 1
                        found_category = node_category

                #### If we did not find exactly one matching category, error out
                if n_found_categorys == 0:
                    response.error(f"Tried to find categorys '{categorys}' for KnowledgeGraph node {key} in query_graph_info, but did not find it", error_code="NodeCategoryMissingInQueryGraph")
                    return response
                elif n_found_categorys > 1:
                    response.error(f"Tried to find categorys '{categorys}' for KnowledgeGraph node {key} in query_graph_info, and found multiple matches. This is ambiguous", error_code="MultipleNodeCategorysInQueryGraph")
                    return response

                #### Else add it
                node.qnode_id = query_graph_info.node_category_map[found_category]

        #### Loop through the edges adding qedge_ids
        for key,edge in edges.items():

            #### Check to see if there is already a qedge_id attribute on the edge
            if edge.qedge_id is None:

                #### If there isn't a predicate or can't find it in the query_graph, error out
                if edge.predicate is None:
                    response.error(f"KnowledgeGraph edge {key} does not have a predicate. This should never be", error_code="EdgeMissingPredicate")
                    return response
                if edge.predicate not in query_graph_info.edge_predicate_map:
                    response.error(f"Tried to find predicate '{edge.predicate}' for KnowledgeGraph node {key} in query_graph_info, but did not find it", error_code="EdgePredicateMissingInQueryGraph")
                    return response

                #### Else add it
                edge.qedge_id = query_graph_info.edge_predicate_map[edge.predicate]

        #### Return the response
        return response



##########################################################################################
def main():

    #### Create a response object
    response = ARAXResponse()

    #### Create an ActionsParser object
    from actions_parser import ActionsParser
    actions_parser = ActionsParser()
 
    #### Set a simple list of actions
    actions_list = [
        "filter(start_node=1, maximum_results=10, minimum_confidence=0.5)",
        "return(message=true,store=false)"
    ]

    #### Parse the action_list and print the result
    result = actions_parser.parse(actions_list)
    response.merge(result)
    if result.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response
    actions = result.data['actions']

    #### Read message #2 from the database. This should be the acetaminophen proteins query result message
    sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../UI/Feedback")
    from RTXFeedback import RTXFeedback
    araxdb = RTXFeedback()
    message_dict = araxdb.getMessage(2)

    #### The stored message comes back as a dict. Transform it to objects
    from ARAX_messenger import ARAXMessenger
    message = ARAXMessenger().from_dict(message_dict)
 
    #### Asses some information about the QueryGraph
    query_graph_info = QueryGraphInfo()
    result = query_graph_info.assess(message)
    response.merge(result)
    if result.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response
    #print(json.dumps(ast.literal_eval(repr(query_graph_info.node_order)),sort_keys=True,indent=2))

    #### Assess some information about the KnowledgeGraph
    knowledge_graph_info = KnowledgeGraphInfo()
    result = knowledge_graph_info.check_for_query_graph_tags(message,query_graph_info)
    response.merge(result)
    if result.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response

    #### Try to add query_graph_ids to the KnowledgeGraph
    result = knowledge_graph_info.add_query_graph_tags(message,query_graph_info)
    response.merge(result)
    if result.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response

    #### Reassess some information about the KnowledgeGraph
    result = knowledge_graph_info.check_for_query_graph_tags(message,query_graph_info)
    response.merge(result)
    if result.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response


    print(response.show(level=ARAXResponse.DEBUG))

    tmp = { 'query_graph_id_node_status': knowledge_graph_info.query_graph_id_node_status, 'query_graph_id_edge_status': knowledge_graph_info.query_graph_id_edge_status, 
        'n_nodes': knowledge_graph_info.n_nodes, 'n_edges': knowledge_graph_info.n_edges }
    #print(json.dumps(message.to_dict(),sort_keys=True,indent=2))
    print(json.dumps(ast.literal_eval(repr(tmp)),sort_keys=True,indent=2))




if __name__ == "__main__": main()


# code/ARAX/ARAXQuery/operation_to_ARAXi.py

# This will be a translation table between the Operations JSON spec (https://github.com/NCATSTranslator/OperationsAndWorkflows/) and ARAXi
import json
import itertools
import re



def _make_expand_query(KP_name, qedge_keys):
    ARAXi = []
    araxi_string = ""
    if not KP_name:
        araxi_string = "expand("
    else:
        araxi_string = f"expand(kp={KP_name}"
    if not qedge_keys:
        pass
    else:
        araxi_string += ","
        araxi_string += re.sub("['\"]", "", f"edge_key={qedge_keys}")
    araxi_string += ")"
    print(f"ARAXi: {araxi_string}")
    return araxi_string

class WorkflowToARAXi:
    def __init__(self):
        self.implemented = {'lookup',
                            'lookup_and_score',
                            'overlay_compute_ngd',
                            'overlay_compute_jaccard',
                            'overlay_fisher_exact_test',
                            'overlay_connect_knodes',
                            'filter_results_top_n',
                            'bind',
                            'fill',
                            'filter_kgraph_orphans',
                            'filter_kgraph_top_n',
                            'filter_kgraph_std_dev',
                            'filter_kgraph_percentile',
                            'filter_kgraph_discrete_kedge_attribute',
                            'filter_kgraph_continuous_kedge_attribute',
                            'sort_results_score',
                            'sort_results_edge_attribute',
                            'sort_results_node_attribute',
                            'annotate_nodes',
                            'score',
                            'complete_results'}

    # NGD
    @staticmethod
    def __translate_overlay_compute_ngd(parameters, query_graph, response):
        if ("virtual_relation_label" not in parameters) or ("qnode_keys" not in parameters):
            response.error("The operation overlay_compute_ngd must have the parameters virtual_relation_label and qnode_keys", error_code="KeyError")
        ARAXi = []
        # loop over all pairs of qnode keys and write the ARAXi
        for source, target in itertools.combinations(parameters["qnode_keys"], 2):
            # TODO: make is so ARAX properly handles -1 as the default value (in ranker)
            ARAXi.append(f"overlay(action=compute_ngd,default_value=inf,virtual_relation_label={parameters['virtual_relation_label']},subject_qnode_key={source},object_qnode_key={target})")
        return ARAXi

    @staticmethod
    def __translate_overlay_connect_knodes(parameters, query_graph, response):
        ARAXi = []
        if len(query_graph['nodes']) >= 3:
            response.warning("This query graph has 3 or more nodes. This may take a while")
        nodes_keys_with_categories = [key for key,node in query_graph['nodes'].items() if node['categories'] is not None]
        qnode_pairs = itertools.combinations(nodes_keys_with_categories,2)
        ARAXi.append(f"overlay(action=compute_ngd,default_value=inf,virtual_relation_label=connect_knodes_ngd)")
        ARAXi.append(f"overlay(action=overlay_clinical_info,COHD_method=paired_concept_frequency,virtual_relation_label=connect_knodes_paired_freq)")
        #ARAXi.append(f"overlay(action=overlay_clinical_info,COHD_method=observed_expected_ratio,virtual_relation_label=connect_knodes_obs_exp)")
        #ARAXi.append(f"overlay(action=overlay_clinical_info,COHD_method=chi_square,virtual_relation_label=connect_knodes_chi_sqr)")
        ARAXi.append(f"overlay(action=predict_drug_treats_disease,virtual_relation_label=connect_knodes_pred_dtd)")
        for qnode_pair in qnode_pairs:
            ARAXi.append(f"overlay(action=fisher_exact_test,virtual_relation_label=connect_knodes_fisher,subject_qnode_key={qnode_pair[0]},object_qnode_key={qnode_pair[1]})")
            ARAXi.append(f"overlay(action=fisher_exact_test,virtual_relation_label=connect_knodes_fisher,subject_qnode_key={qnode_pair[1]},object_qnode_key={qnode_pair[0]})")
        if len(query_graph['nodes']) >= 3:
            qnode_triples = itertools.combinations(query_graph['nodes'].keys(),3)
            for qnode_triple in qnode_triples:
                ARAXi.append(f"overlay(action=compute_jaccard,virtual_relation_label=connect_knodes_jaccard,start_node_key={qnode_triple[0]},end_node_key={qnode_triple[1]},intermediate_node_key={qnode_triple[2]})")
                ARAXi.append(f"overlay(action=compute_jaccard,virtual_relation_label=connect_knodes_jaccard,start_node_key={qnode_triple[2]},end_node_key={qnode_triple[0]},intermediate_node_key={qnode_triple[1]})")
                ARAXi.append(f"overlay(action=compute_jaccard,virtual_relation_label=connect_knodes_jaccard,start_node_key={qnode_triple[1]},end_node_key={qnode_triple[2]},intermediate_node_key={qnode_triple[0]})")
        return ARAXi

    @staticmethod
    def __translate_overlay_compute_jaccard(parameters, query_graph, response):
        if ("virtual_relation_label" not in parameters) or ("end_node_keys" not in parameters) or ("intermediate_node_key" not in parameters):
            response.error("The operation overlay_compute_jaccard must have the parameters virtual_relation_label, end_node_keys, and intermediate_node_key", error_code="KeyError")
        ARAXi = []
        source = parameters['end_node_keys'][0]
        target = parameters['end_node_keys'][1]
        ARAXi.append(f"overlay(action=compute_jaccard,virtual_relation_label={parameters['virtual_relation_label']},start_node_key={source},end_node_key={target},intermediate_node_key={parameters['intermediate_node_key']})")
        return ARAXi

    @staticmethod
    def __translate_overlay_fisher_exact_test(parameters, query_graph, response):
        if ("virtual_relation_label" not in parameters) or ("subject_qnode_key" not in parameters) or ("object_qnode_key" not in parameters):
            response.error("The operation overlay_fisher_exact_test must have the parameters virtual_relation_label, subject_qnode_key, and object_qnode_key", error_code="KeyError")
        ARAXi = []
        if 'rel_edge_key' not in parameters:
            ARAXi.append(f"overlay(action=fisher_exact_test,virtual_relation_label={parameters['virtual_relation_label']},subject_qnode_key={parameters['subject_qnode_key']},object_qnode_key={parameters['object_qnode_key']})")
        else:
            ARAXi.append(f"overlay(action=fisher_exact_test,virtual_relation_label={parameters['virtual_relation_label']},subject_qnode_key={parameters['subject_qnode_key']},object_qnode_key={parameters['object_qnode_key']},rel_edge_key={parameters['rel_edge_key']})")
        return ARAXi

    @staticmethod
    def __translate_filter_results_top_n(parameters, query_graph, response):
        if 'max_results' not in parameters:
            response.error("The operation filter_results_top_n must have the parameter max_results", error_code="KeyError")
        assert type(parameters['max_results']) == int
        ARAXi = []
        ARAXi.append(f"filter_results(action=limit_number_of_results,max_results={parameters['max_results']},prune_kg=true)")  # prune the kg
        return ARAXi

    @staticmethod
    def __translate_bind(parameters, query_graph, response):
        ARAXi = []
        ARAXi.append(f"scoreless_resultify(ignore_edge_direction=true)")  # ignore edge directions
        return ARAXi

    @staticmethod
    def __translate_fill(parameters, query_graph, response):
        if 'denylist' in parameters:
            response.error("ARAX has not implementer the parameter denylist", error_code="NotImplementedError")
        ARAXi = []
        araxi_string = ""
        if 'allowlist' in parameters:
            for KP_name in parameters['allowlist']:
                ARAXi.append(_make_expand_query(KP_name, parameters.get('qedge_keys')))
        else:
            araxi_string += "expand("
            if "qedge_keys" in parameters:
                if not araxi_string.endswith("("):
                    araxi_string += ","
                araxi_string += re.sub("['\"]","",f"edge_key={parameters['qedge_keys']}")
            araxi_string += ")"
        if araxi_string:
            print(f"araxi_string2: {araxi_string}")
            ARAXi.append(araxi_string)
        return ARAXi


    @staticmethod
    def __translate_filter_kgraph_orphans(parameters, query_graph, response):
        ARAXi = []
        ARAXi.append(f"filter_kg(action=remove_orphaned_nodes)")
        return ARAXi

    @staticmethod
    def __translate_filter_kgraph_top_n(parameters, query_graph, response):
        if ("edge_attribute" not in parameters):
            response.error("The operation filter_kgraph_top_n must have the parameter edge_attribute", error_code="KeyError")
        ARAXi = []
        threshold = parameters.get('max_edges',50)
        top = parameters.get('keep_top_or_bottom','top')
        if top == 'top':
            direction = 'below'
        else:
            direction = 'above'
        # FW: need to update this to handle qedge_keys and qnode_keys
        araxi_string = f"filter_kg(action=remove_edges_by_top_n,edge_attribute={parameters['edge_attribute']},n={threshold},direction={direction},top={top == 'top'}"
        if "qnode_keys" in parameters:
            araxi_string += re.sub("['\"]","",f",remove_connected_nodes=t,qnode_keys={parameters['qnode_keys']}")
        if "qedge_keys" in parameters:
            araxi_string += re.sub("['\"]","",f",qedge_keys={parameters['qedge_keys']}")
        araxi_string += ")"
        ARAXi.append(araxi_string)
        return ARAXi

    @staticmethod
    def __translate_filter_kgraph_std_dev(parameters, query_graph, response):
        if ("edge_attribute" not in parameters):
            response.error("The operation filter_kgraph_std_dev must have the parameter edge_attribute", error_code="KeyError")
        ARAXi = []
        threshold = parameters.get('threshold',1)
        direction = parameters.get('remove_above_or_below','below')
        top = parameters.get('keep_top_or_bottom','top')
        # FW: need to update this to handle qedge_keys and qnode_keys
        araxi_string = f"filter_kg(action=remove_edges_by_std_dev,edge_attribute={parameters['edge_attribute']},threshold={threshold},direction={direction},top={top == 'top'}"
        if "qnode_keys" in parameters:
            araxi_string += re.sub("['\"]","",f",remove_connected_nodes=t,qnode_keys={parameters['qnode_keys']}")
        if "qedge_keys" in parameters:
            araxi_string += re.sub("['\"]","",f",qedge_keys={parameters['qedge_keys']}")
        araxi_string += ")"
        ARAXi.append(araxi_string)
        return ARAXi

    @staticmethod
    def __translate_filter_kgraph_percentile(parameters, query_graph, response):
        if ("edge_attribute" not in parameters):
            response.error("The operation filter_kgraph_percentile must have the parameter edge_attribute", error_code="KeyError")
        ARAXi = []
        threshold = parameters.get('threshold',95)
        direction = parameters.get('remove_above_or_below','below')
        # FW: need to update this to handle qedge_keys and qnode_keys
        araxi_string = f"filter_kg(action=remove_edges_by_percentile,edge_attribute={parameters['edge_attribute']},threshold={threshold},direction={direction}"
        if "qnode_keys" in parameters:
            araxi_string += re.sub("['\"]","",f",remove_connected_nodes=t,qnode_keys={parameters['qnode_keys']}")
        if "qedge_keys" in parameters:
            araxi_string += re.sub("['\"]","",f",qedge_keys={parameters['qedge_keys']}")
        araxi_string += ")"
        ARAXi.append(araxi_string)
        return ARAXi

    @staticmethod
    def __translate_filter_kgraph_continuous_kedge_attribute(parameters, query_graph, response):
        if ("edge_attribute" not in parameters) or ("threshold" not in parameters) or ("remove_above_or_below" not in parameters):
            response.error("The operation kgraph_continuous_attribute must have the parameters edge_attribute, threshold, and remove_above_or_below", error_code="KeyError")
        ARAXi = []
        threshold = parameters.get('threshold',None)
        direction = parameters.get('remove_above_or_below',None)
        # FW: need to update this to handle qedge_keys and qnode_keys
        araxi_string = f"filter_kg(action=remove_edges_by_continuous_attribute,edge_attribute={parameters['edge_attribute']},threshold={threshold},direction={direction}"
        if "qnode_keys" in parameters:
            araxi_string += re.sub("['\"]","",f",remove_connected_nodes=t,qnode_keys={parameters['qnode_keys']}")
        if "qedge_keys" in parameters:
            araxi_string += re.sub("['\"]","",f",qedge_keys={parameters['qedge_keys']}")
        araxi_string += ")"
        ARAXi.append(araxi_string)
        return ARAXi

    @staticmethod
    def __translate_filter_kgraph_discrete_kedge_attribute(parameters, query_graph, response):
        if ("edge_attribute" not in parameters) or ("remove_value" not in parameters):
            response.error("The operation kgraph_continuous_attribute must have the parameters edge_attribute and remove_value", error_code="KeyError")
        ARAXi = []
        value = parameters.get('remove_value',None)
        # FW: need to update this to handle qedge_keys and qnode_keys
        araxi_string = f"filter_kg(action=remove_edges_by_discrete_attribute,edge_attribute={parameters['edge_attribute']},value={value}"
        if "qnode_keys" in parameters:
            araxi_string += re.sub("['\"]","",f",remove_connected_nodes=t,qnode_keys={parameters['qnode_keys']}")
        if "qedge_keys" in parameters:
            araxi_string += re.sub("['\"]","",f",qedge_keys={parameters['qedge_keys']}")
        araxi_string += ")"
        ARAXi.append(araxi_string)
        return ARAXi

    @staticmethod
    def __translate_sort_results_edge_attribute(parameters, query_graph, response):
        if ("edge_attribute" not in parameters) or ("ascending_or_descending" not in parameters):
            response.error("The operation sort_results_edge_attribute must have the parameters edge_attribute and ascending_or_descending", error_code="KeyError")
        ARAXi = []
        # FW: need to update this to handle qedge_keys and qnode_keys
        araxi_string = f"filter_results(action=sort_by_edge_attribute,edge_attribute={parameters['edge_attribute']},direction={ascending_or_descending}"
        if "qedge_keys" in parameters and parameters['qedge_keys'] is not None:
            araxi_string += re.sub("['\"]","",f",qedge_keys={parameters['qedge_keys']}")
        araxi_string += ")"
        ARAXi.append(araxi_string)
        return ARAXi

    @staticmethod
    def __translate_sort_results_node_attribute(parameters, query_graph, response):
        if ("node_attribute" not in parameters) or ("ascending_or_descending" not in parameters):
            response.error("The operation sort_results_node_attribute must have the parameters node_attribute and ascending_or_descending", error_code="KeyError")
        ARAXi = []
        # FW: need to update this to handle qedge_keys and qnode_keys
        araxi_string = f"filter_results(action=sort_by_node_attribute,node_attribute={parameters['node_attribute']},direction={ascending_or_descending}"
        if "qnode_keys" in parameters and parameters['qnode_keys'] is not None:
            araxi_string += re.sub("['\"]","",f",remove_connected_nodes=t,qnode_keys={parameters['qnode_keys']}")
        araxi_string += ")"
        ARAXi.append(araxi_string)
        return ARAXi

    @staticmethod
    def __translate_sort_results_score(parameters, query_graph, response):
        if ("ascending_or_descending" not in parameters):
            response.error("The operation sort_results_score must have the parameter ascending_or_descending", error_code="KeyError")
        ARAXi = []
        araxi_string = f"filter_results(action=sort_by_score,direction={parameters['ascending_or_descending']}"
        araxi_string += ")"
        ARAXi.append(araxi_string)
        return ARAXi

    @staticmethod
    def __translate_annotate_nodes(parameters, query_graph, response):
        ARAXi = []
        attributes = parameters.get('attributes',None)
        if attributes is None or 'pmids' in attributes:
            araxi_string = f"overlay(action=add_node_pmids)"
            ARAXi.append(araxi_string)
        return ARAXi

    def translate(self, workflow, query_graph, response):
        ARAXi = []
        for operation in workflow:
            if operation['id'] not in self.implemented:
                response.error("This operation has not yet been implemented to the workflow to ARAXi translator", error_code="NotImplementedError")
                # immediately return since we don't know what to do with an operation that isn't implemented
                return
            if 'parameters' in operation:
                ARAXi.extend(getattr(self, '_' + self.__class__.__name__ + '__translate_' + operation['id'])(operation['parameters'], query_graph, response))
            else:
                ARAXi.extend(getattr(self, '_' + self.__class__.__name__ + '__translate_' + operation['id'])({}, query_graph, response))
        return ARAXi

    @staticmethod
    def __translate_score(parameters, query_graph, response):
        ARAXi = []
        ARAXi.append(f"rank_results()")
        return ARAXi

    @staticmethod
    def __translate_complete_results(parameters, query_graph, response):
        ARAXi = []
        ARAXi.append(f"scoreless_resultify(ignore_edge_direction=true)")  # ignore edge directions
        return ARAXi

    @staticmethod
    def __translate_lookup(parameters, query_graph, response):
        ARAXi = []
        ARAXi.append("expand()")
        ARAXi.append(f"scoreless_resultify(ignore_edge_direction=true)")  # ignore edge directions
        return ARAXi

    @staticmethod
    def __translate_lookup_and_score(parameters, query_graph, response):
        ARAXi = []
        ARAXi.append("expand()")
        ARAXi.append(f"resultify(ignore_edge_direction=true)")  # ignore edge directions
        return ARAXi



def main():
    trapi_eg = """{
        "query_graph": {
            "nodes": {
                "type-2 diabetes": {
                    "id": "MONDO:0005148"
                },
                "drug": {
                    "category": "biolink:ChemicalSubstance"
                }
            },
            "edges": {
                "treats": {
                    "subject": "drug",
                    "predicate": "biolink:treats",
                    "object": "type-2 diabetes"
                }
            }
        },
        "knowledge_graph": {
            "nodes": {
                "MONDO:0005148": {"name": "type-2 diabetes"},
                "CHEBI:6801": {
                    "name": "metformin",
                    "category": "biolink:ChemicalSubstance"
                },
                "CHEBI:5441": {
                    "name": "glyburide",
                    "category": "biolink:ChemicalSubstance"
                }
            },
            "edges": {
                "df87ff82": {
                    "subject": "CHEBI:6801",
                    "predicate": "biolink:treats",
                    "object": "MONDO:0005148"
                },
                "5133c100": {
                    "subject": "CHEBI:5441",
                    "predicate": "biolink:treats",
                    "object": "MONDO:0005148"
                }
            }
        },
        "results": [
            {
                "node_bindings": {
                    "type-2 diabetes": [{"id": "MONDO:0005148"}],
                    "drug": [{"id": "CHEBI:6801"}]
                },
                "edge_bindings": {
                    "treats": [{"id": "df87ff82"}]
                }
            }
        ],
        "workflow": [
          {
            "id": "overlay_compute_ngd",
            "parameters": {
                "virtual_relation_label": "ngd1",
                "qnode_keys": ["drug", "type-2 diabetes"]
            }
          },
          {
            "id": "filter_results_top_n",
            "parameters": {
                "max_results": 12
            }
          }
        ]
    }"""

    test_trapi = json.loads(trapi_eg)
    print(f"Workflow: {test_trapi['workflow']}")
    W = WorkflowToARAXi()
    print(f"ARAXi: {W.translate(test_trapi['workflow'])}")

if __name__ == "__main__": main()


# code/ARAX/ARAXQuery/query_graph_info.py

#!/bin/env python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast
import re

from ARAX_response import ARAXResponse

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../NodeSynonymizer")
from node_synonymizer import NodeSynonymizer

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../UI/OpenAPI/python-flask-server/")
from openapi_server.models.query_graph import QueryGraph


class QueryGraphInfo:

    #### Constructor
    def __init__(self):
        self.response = None
        self.message = None

        self.n_nodes = None
        self.n_edges = None
        self.is_bifurcated_graph = False
        self.start_node = None
        self.node_info = None
        self.edge_info = None
        self.node_order = None
        self.edge_order = None
        self.node_category_map = None
        self.edge_predicate_map = None

        self.query_graph_templates = None


    #### Top level decision maker for applying filters
    def assess(self, message):

        #### Define a default response
        response = ARAXResponse()
        self.response = response
        self.message = message
        response.debug(f"Assessing the QueryGraph for basic information")

        #### Get shorter handles
        query_graph = message.query_graph
        nodes = {}
        if query_graph.nodes is not None:
            nodes = query_graph.nodes
        edges = {}
        if query_graph.edges is not None:
            edges = query_graph.edges

        #### Store number of nodes and edges
        self.n_nodes = len(nodes)
        self.n_edges = len(edges)
        response.debug(f"Found {self.n_nodes} nodes and {self.n_edges} edges")

        #### Handle impossible cases
        if self.n_nodes == 0:
            response.error("QueryGraph has 0 nodes. At least 1 node is required", error_code="QueryGraphZeroNodes")
            return response
        if self.n_nodes == 1 and self.n_edges > 0:
            response.error("QueryGraph may not have edges if there is only one node", error_code="QueryGraphTooManyEdges")
            return response


        #### Loop through nodes computing some stats
        have_at_least_one_id = False
        node_info = {}
        self.node_category_map = {}
        for key,qnode in nodes.items():

            if key is None:
                response.error("QueryGraph has a node with null key. This is not permitted", error_code="QueryGraphNodeWithNullKey")
                return response

            node_info[key] = { 'key': key, 'node_object': qnode, 'has_ids': False, 'categories': qnode.categories, 'has_categories': False, 'is_set': False, 'n_edges': 0, 'n_links': 0, 'is_connected': False, 'edges': [], 'edge_dict': {} }
            if qnode.ids is not None:
                node_info[key]['has_ids'] = True
                have_at_least_one_id = True

                #### If the user did not specify a category, but there is a curie, try to figure out the category
                if node_info[key]['categories'] is None:
                    synonymizer = NodeSynonymizer()
                    curie = qnode.ids
                    curies_list = qnode.ids
                    if isinstance(qnode.ids,list):
                        curie = qnode.ids[0]
                    else:
                        curies_list = [ qnode.ids ]

                    canonical_curies = synonymizer.get_canonical_curies(curies=curies_list, return_all_categories=True)
                    response.debug(f"canonical_curies={canonical_curies}, curie={curie}")
                    if curie in canonical_curies and canonical_curies[curie] is not None and 'preferred_type' in canonical_curies[curie]:
                        node_info[key]['has_categories'] = True
                        node_info[key]['categories'] = canonical_curies[curie]['preferred_type']

            if qnode.categories is not None:
                node_info[key]['has_categories'] = True

            #if qnode.is_set is not None: node_info[key]['is_set'] = True

            #### Store lookup of categories
            warning_counter = 0
            if qnode.categories is None or ( isinstance(qnode.categories,list) and len(qnode.categories) == 0 ):
                if warning_counter == 0:
                    #response.debug("QueryGraph has nodes with no categories. This may cause problems with results inference later")
                    pass
                warning_counter += 1
                self.node_category_map['unknown'] = key
            else:
                category = qnode.categories
                if isinstance(qnode.categories,list):
                    category = qnode.categories[0]                # FIXME this is a hack prior to proper list handling
                self.node_category_map[category] = key


        #### If we don't even have one id, then we don't support this
        if not have_at_least_one_id:
            response.error("QueryGraph has no nodes with ids. At least one node must have a specified 'ids'", error_code="QueryGraphNoIds")
            return response


        #### Ignore special informationational edges for now.
        virtual_edge_predicates = {
            'biolink:has_normalized_google_distance_with': 1,
            'biolink:has_fisher_exact_test_p-value_with': 1,
            'biolink:has_jaccard_index_with': 1,
            'biolink:probably_treats': 1,
            'biolink:has_paired_concept_frequency_with': 1,
            'biolink:has_observed_expected_ratio_with': 1,
            'biolink:has_chi_square_with': 1
            }


        #### Loop through edges computing some stats
        edge_info = {}
        self.edge_predicate_map = {}
        unique_links = {}

        for key,qedge in edges.items():

            predicate = qedge.predicates
            if isinstance(predicate,list):
                if len(predicate) == 0:
                    predicate = None
                else:
                    predicate = predicate[0]                       # FIXME Hack before dealing with predicates as lists!

            if predicate is not None and predicate in virtual_edge_predicates:
                continue

            edge_info[key] = { 'key': key, 'has_predicates': False, 'subject': qedge.subject, 'object': qedge.object, 'predicates': None, 'exclude': False }

            if qedge.exclude is not None:
                edge_info[key]['exclude'] = qedge.exclude

            if predicate is not None:
                edge_info[key]['has_predicates'] = True
                edge_info[key]['predicates'] = predicate

            if key is None:
                response.error("QueryGraph has a edge with null key. This is not permitted", error_code="QueryGraphEdgeWithNoKey")
                return response

            #### Create a unique node link string
            link_string = ','.join(sorted([qedge.subject,qedge.object]))
            if link_string not in unique_links:
                if qedge.subject not in node_info:
                    response.error(f"QEdge subject={qedge.subject} is not found among the nodes", error_code="QEdgeInvalidSubject")
                    return response
                node_info[qedge.subject]['n_links'] += 1
                if qedge.object not in node_info:
                    response.error(f"QEdge object={qedge.object} is not found among the nodes", error_code="QEdgeInvalidObject")
                    return response
                node_info[qedge.object]['n_links'] += 1
                unique_links[link_string] = 1
                #print(link_string)

            node_info[qedge.subject]['n_edges'] += 1
            node_info[qedge.object]['n_edges'] += 1
            node_info[qedge.subject]['is_connected'] = True
            node_info[qedge.object]['is_connected'] = True
            #node_info[qedge.subject]['edges'].append(edge_info[key])
            #node_info[qedge.object]['edges'].append(edge_info[key])
            node_info[qedge.subject]['edges'].append(edge_info[key])
            node_info[qedge.object]['edges'].append(edge_info[key])
            node_info[qedge.subject]['edge_dict'][key] = edge_info[key]
            node_info[qedge.object]['edge_dict'][key] = edge_info[key]

            #### Store lookup of predicates
            warning_counter = 0
            edge_predicate = 'any'
            if predicate is None:
                if warning_counter == 0:
                    response.debug("QueryGraph has edges with no predicate. This may cause problems with results inference later")
                warning_counter += 1
            else:
                edge_predicate = predicate

            #### It's not clear yet whether we need to store the whole sentence or just the predicate
            #predicate_encoding = f"{node_info[qedge.subject]['predicate']}---{edge_predicate}---{node_info[qedge.object]['predicate']}"
            predicate_encoding = edge_predicate
            self.edge_predicate_map[predicate_encoding] = key


        #### Loop through the nodes again, trying to identify the start_node and the end_node
        singletons = []
        for node_id,node_data in node_info.items():
            if node_data['n_links'] < 2:
                singletons.append(node_data)
            elif node_data['n_links'] > 2:
                self.is_bifurcated_graph = True
                response.warning("QueryGraph appears to have a fork in it. This might cause trouble")

        #### If this doesn't produce any singletons, then try curie based selection
        if len(singletons) == 0:
            for node_id,node_data in node_info.items():
                if node_data['has_ids']:
                    singletons.append(node_data)

        #### If this doesn't produce any singletons, then we don't know how to continue
        if len(singletons) == 0:
            response.error("Unable to understand the query graph", error_code="QueryGraphCircular")
            return response

        #### Try to identify the start_node and the end_node
        start_node = singletons[0]
        if len(nodes) == 1:
            # Just a single node, fine
            pass
        elif len(singletons) < 2:
            response.warning("QueryGraph appears to be circular or has a strange geometry. This might cause trouble")
        elif len(singletons) > 2:
            response.warning("QueryGraph appears to have a fork in it. This might cause trouble")
        else:
            if singletons[0]['has_ids'] is True and singletons[1]['has_ids'] is False:
                start_node = singletons[0]
            elif singletons[0]['has_ids'] is False and singletons[1]['has_ids'] is True:
                start_node = singletons[1]
            else:
                start_node = singletons[0]
        #### Hmm, that's not very robust against odd graphs. This needs work. FIXME

        #### Store results into the object
        self.node_info = node_info
        self.edge_info = edge_info
        self.start_node = start_node


        #### Set up state for computing the node order
        current_node = start_node
        node_order = [ start_node ]
        edge_order = [ ]
        edges = current_node['edges']
        debug = False
        loop_counter = 0
        visited_nodes = { current_node['key']: True }

        #### Starting with the start node, loop until we run out of nodes to create node_order
        while 1:

            if debug:
                tmp = { 'astate': '1', 'current_node': current_node, 'node_order': node_order, 'edge_order': edge_order, 'edges': edges }
                print('==================================================================================')
                print(json.dumps(ast.literal_eval(repr(tmp)),sort_keys=True,indent=2))
                #tmp = input()

            #### Ensure that we don't get stuck in an infinite loop
            loop_counter += 1
            if loop_counter > 20:
                response.error(f"Reached loop max: {loop_counter}", error_code="InteralError_F260")
                return response

            #### If we arrive at a node that has no more edges going out, then we're done
            if len(edges) == 0:
                break

            #### If there are multiple edges here, then we just take the first one. FIXME ignore the rest
            edge_order.append(edges[0])
            previous_node = current_node
            if edges[0]['subject'] == current_node['key']:
                current_node = node_info[edges[0]['object']]
            elif edges[0]['object'] == current_node['key']:
                current_node = node_info[edges[0]['subject']]
            else:
                response.error("Help, edge error A584. Don't know what to do", error_code="InteralErrorA584")
                return response

            #### If we've already been there, then this is a circular graph and just end here
            if current_node['key'] in visited_nodes:
                response.warning("Fell into a loop in the QueryGraph. Picking an end node somewhat arbitrarily.")
                break
            visited_nodes[current_node['key']] = True

            node_order.append(current_node)

            #tmp = { 'astate': '2', 'current_node': current_node, 'node_order': node_order, 'edge_order': edge_order, 'edges': edges }
            #print(json.dumps(ast.literal_eval(repr(tmp)),sort_keys=True,indent=2))
            #print('==================================================================================')
            #tmp = input()

            edges = current_node['edges']
            new_edges = []
            for edge in edges:
                key = edge['key']
                if key not in previous_node['edge_dict']:
                    new_edges.append(edge)
            edges = new_edges
            if len(edges) == 0:
                break
            #tmp = { 'astate': '3', 'current_node': current_node, 'node_order': node_order, 'edge_order': edge_order, 'edges': edges }
            #print(json.dumps(ast.literal_eval(repr(tmp)),sort_keys=True,indent=2))
            #print('==================================================================================')
            #tmp = input()


        self.node_order = node_order
        self.edge_order = edge_order


        #### Loop over the ordered list of nodes,
        #### creating a text rendering of the QueryGraph geometry for matching against a template
        self.query_graph_templates = { 'simple': '', 'detailed': { 'n_nodes': len(node_order), 'components': [] } }
        node_index = 0
        edge_index = 0

        for node in node_order:
            component_id = f"n{node_index:02}"
            content = ''
            component = { 'component_type': 'node', 'component_id': component_id, 'has_ids': node['has_ids'], 'has_categories': node['has_categories'], 'categories_value': None }
            self.query_graph_templates['detailed']['components'].append(component)
            if node['has_ids']:
                content = 'ids'
            elif node['has_categories'] and node['node_object'].categories is not None:
                category = node['node_object'].categories
                if isinstance(category,list):
                    category = category[0]                                      # FIXME: Can we be smarter than just taking the first?
                content = f"categories={category}"
                component['categories_value'] = node['node_object'].categories
            elif node['has_categories']:
                content = 'categories'
            template_part = f"{component_id}({content})"
            self.query_graph_templates['simple'] += template_part

            # Since queries with intermediate nodes that are not is_set=true tend to blow up,
            # we thought it would be a good idea to set them to true, but then around 2021-07
            # this seemed to hurt more than it helps, so disable it for now and see how that goes.
            # Intermediate nodes that have ids (curies) are an exception to this treatment
            if node_index > 0 and node_index < (self.n_nodes - 1 ) and node['has_ids'] == False:
                if 'is_set' not in node or node['is_set'] is None:
                    #node['node_object'].is_set = True
                    #response.warning(f"Setting unspecified is_set to true for {node['key']} because this will probably lead to a happier result")
                    response.info(f"Property is_set is not true for {node['key']} although maybe it should be to keep the query from blowing up. Consider setting is_set to true here.")
                elif node['is_set'] is True:
                    response.debug(f"Value for is_set is already true for {node['key']} so that's good")
                elif node['is_set'] is False:
                    #node['node_object'].is_set = True
                    #response.info(f"Value for is_set is false for intermediate node {node['key']}. Setting to true because this will probably lead to a happier result")
                    response.info(f"Property is_set is not true for {node['key']} although maybe it should be to keep the query from blowing up. Consider setting is_set to true here.")

            node_index += 1
            if node_index < self.n_nodes:
                #print(json.dumps(ast.literal_eval(repr(node)),sort_keys=True,indent=2))

                #### Extract the has_predicate and predicate_value from the edges of the node
                #### This could fail if there are two edges coming out of the node FIXME
                has_predicates = False
                predicates_value = None
                if 'edges' in node:
                    for related_edge in node['edges']:
                        if related_edge['subject'] == node['key']:
                            has_predicates = related_edge['has_predicates']
                            if has_predicates is True and 'predicates' in related_edge:
                                predicates_value = related_edge['predicates']

                component_id = f"e{edge_index:02}"
                template_part = f"-{component_id}()-"
                self.query_graph_templates['simple'] += template_part
                component = { 'component_type': 'edge', 'component_id': component_id, 'has_ids': False, 'has_predicates': has_predicates, 'predicates_value': predicates_value }
                self.query_graph_templates['detailed']['components'].append(component)
                edge_index += 1

        response.debug(f"The QueryGraph reference template is: {self.query_graph_templates['simple']}")

        #tmp = { 'node_info': node_info, 'edge_info': edge_info, 'start_node': start_node, 'n_nodes': self.n_nodes, 'n_edges': self.n_edges,
        #    'is_bifurcated_graph': self.is_bifurcated_graph, 'node_order': node_order, 'edge_order': edge_order }
        #print(json.dumps(ast.literal_eval(repr(tmp)),sort_keys=True,indent=2))
        #sys.exit(0)

        #### Return the response
        return response


##########################################################################################
def test_example1():

    test_query_graphs = [
        { "description": "Two nodes, one edge linking them, 1 CURIE",
          "nodes": {
            "n00": { "ids": [ "MONDO:0001627" ] },
            "n01": { "categories": [ "biolink:ChemicalEntity" ] } },
          "edges": {
            "e00": { "subject": "n00", "object": "n01", "predicates": [ "biolink:physically_interacts_with" ] } } },

        { "description": "Two nodes, two edges linking them, 1 CURIE, one of which is excluded",
          "nodes": {
            "n00": { "ids": [ "MONDO:0001627" ] },
            "n01": { "categories": [ "biolink:ChemicalEntity" ] } },
          "edges": {
            "e00": { "subject": "n00", "object": "n01" },
            "e01": { "subject": "n00", "object":"n01", "predicates": [ "biolink:contraindicated_for" ], "exclude": True } } },

        { "description": "Two nodes, one edge linking them, both nodes are CURIEs",
          "nodes": {
            "n00": { "ids": [ "MONDO:0001627" ] },
            "n01": { "ids": [ "CHEMBL.COMPOUND:CHEMBL112" ] } },
          "edges": {
            "e00": { "subject": "n00", "object": "n01" } } },

        { "description": "Three nodes, 2 edges, 1 CURIE, simple linear chain",
          "nodes": {
            "n00": { "ids": [ "MONDO:0001627" ] },
            "n01": { "categories": [ "biolink:ChemicalEntity" ] },
            "n02": { "categories": [ "biolink:Protein" ] } },
          "edges": {
            "e00": { "subject": "n00", "object": "n01", "predicates": [ "biolink:physically_interacts_with" ] },
            "e01": { "subject": "n01", "object": "n02" } } },

        { "description": "Three nodes, 2 edges, but the CURIE is in the middle. What does that even mean?",
          "nodes": {
            "n00": { "categories": [ "biolink:ChemicalEntity" ] },
            "n01": { "ids": [ "MONDO:0001627" ] },
            "n02": { "categories": [ "biolink:Protein" ] } },
          "edges": {
            "e00": { "subject": "n00", "object": "n01", "predicates": [ "biolink:physically_interacts_with" ] },
            "e01": { "subject": "n01", "object": "n02" } } },

        { "description": "Four nodes, 3 edges, 1 CURIE, simple linear chain",
          "nodes": {
            "n00": { "ids": [ "MONDO:0001627" ] },
            "n01": { "categories": [ "biolink:ChemicalEntity" ] },
            "n02": { "categories": [ "biolink:Protein" ] },
            "n03": { "categories": [ "biolink:Disease" ] } },
          "edges": {
            "e00": { "subject": "n00", "object": "n01", "predicates": [ "biolink:physically_interacts_with" ] },
            "e01": { "subject": "n01", "object": "n02" },
            "e02": { "subject": "n02", "object": "n03" } } },

        { "description": "Two nodes, one edge linking them, 0 CURIEs",
          "nodes": {
            "n00": { "categories": [ "biolink:Drug" ] },
            "n01": { "categories": [ "biolink:ChemicalEntity" ] } },
          "edges": {
            "e00": { "subject": "n00", "object": "n01", "predicates": [ "biolink:physically_interacts_with" ] } } },

        { "description": "One node only",
          "nodes": {
            "n00": { "ids": [ "MONDO:0001627" ] } },
          "edges": {} },

        ]

    from ARAX_messenger import ARAXMessenger

    for test_query_graph in test_query_graphs:
        response = ARAXResponse()
        messenger = ARAXMessenger()
        messenger.create_envelope(response)

        print('==================================================================')
        description = test_query_graph['description']
        del test_query_graph['description']
        print(f"Query Graph '{description}'")

        response.envelope.message.query_graph = QueryGraph().from_dict(test_query_graph)

        query_graph_info = QueryGraphInfo()
        result = query_graph_info.assess(response.envelope.message)
        response.merge(result)
        if result.status != 'OK':
            print(response.show(level=ARAXResponse.DEBUG))
            return response

        query_graph_info_dict = {
            'n_nodes': query_graph_info.n_nodes,
            'n_edges': query_graph_info.n_edges,
            'is_bifurcated_graph': query_graph_info.is_bifurcated_graph,
            'start_node': query_graph_info.start_node['key'],
            'simple_query_graph_template': query_graph_info.query_graph_templates['simple'],
            #'start_node': query_graph_info.start_node,
            #'node_info': query_graph_info.node_info,
            #'edge_info': query_graph_info.edge_info,
            #'node_order': query_graph_info.node_order,
            #'edge_order': query_graph_info.edge_order,
            #'node_category_map': query_graph_info.node_category_map,
            #'edge_predicate_map': query_graph_info.edge_predicate_map,
        }
        print(json.dumps(ast.literal_eval(repr(query_graph_info_dict)),sort_keys=True,indent=2))



##########################################################################################
def main():

    test_example1()
    return

    #### Create a response object
    response = ARAXResponse()

    #### Create an ActionsParser object
    from actions_parser import ActionsParser
    actions_parser = ActionsParser()
 
    #### Set a simple list of actions
    actions_list = [
        "filter(start_node=1, maximum_results=10, minimum_confidence=0.5)",
        "return(message=true,store=false)"
    ]

    #### Parse the action_list and print the result
    result = actions_parser.parse(actions_list)
    response.merge(result)
    if result.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response
    actions = result.data['actions']

    #### Read message #2 from the database. This should be the acetaminophen proteins query result message
    sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../UI/Feedback")
    from RTXFeedback import RTXFeedback
    araxdb = RTXFeedback()
    message_dict = araxdb.getMessage(2)

    #### The stored message comes back as a dict. Transform it to objects
    from ARAX_messenger import ARAXMessenger
    message = ARAXMessenger().from_dict(message_dict)
    #print(json.dumps(message.to_dict(),sort_keys=True,indent=2))

    #### Create a filter object and use it to apply action[0] from the list
    query_graph_info = QueryGraphInfo()
    result = query_graph_info.assess(message)
    response.merge(result)
    if result.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
        return response
    
    query_graph_info_dict = {
        'n_nodes': query_graph_info.n_nodes,
        'n_edges': query_graph_info.n_edges,
        'is_bifurcated_graph': query_graph_info.is_bifurcated_graph,
        'start_node': query_graph_info.start_node,
        'node_info': query_graph_info.node_info,
        'edge_info': query_graph_info.edge_info,
        'node_order': query_graph_info.node_order,
        'edge_order': query_graph_info.edge_order,
        'node_category_map': query_graph_info.node_category_map,
        'edge_predicate_map': query_graph_info.edge_predicate_map,
    }
    print(json.dumps(ast.literal_eval(repr(query_graph_info_dict)),sort_keys=True,indent=2))


if __name__ == "__main__": main()


# code/ARAX/BiolinkHelper/biolink_helper.py

#!/bin/env python3
"""
Usage:  python biolink_helper.py [biolink version number, e.g. 3.0.3]
"""

import argparse
import json
import os
import pathlib
import pickle
from collections import defaultdict
from typing import Optional, List, Set, Dict, Union, Tuple

import requests
import yaml
from treelib import Tree


class BiolinkHelper:

    def __init__(self, biolink_version: Optional[str] = None):
        self.biolink_version = biolink_version if biolink_version else self.get_current_arax_biolink_version()
        self.root_category = "biolink:NamedThing"
        self.root_predicate = "biolink:related_to"
        biolink_helper_dir = os.path.dirname(os.path.abspath(__file__))
        self.biolink_lookup_map_path = f"{biolink_helper_dir}/biolink_lookup_map_{self.biolink_version}_v3.pickle"
        self.biolink_lookup_map = self._load_biolink_lookup_map()
        protein_like_categories = {"biolink:Protein", "biolink:Gene"}
        disease_like_categories = {"biolink:Disease", "biolink:PhenotypicFeature", "biolink:DiseaseOrPhenotypicFeature"}
        self.arax_conflations = {
            "biolink:Protein": protein_like_categories,
            "biolink:Gene": protein_like_categories,
            "biolink:Disease": disease_like_categories,
            "biolink:PhenotypicFeature": disease_like_categories,
            "biolink:DiseaseOrPhenotypicFeature": disease_like_categories
        }

    def get_ancestors(self, biolink_items: Union[str, List[str]], include_mixins: bool = True, include_conflations: bool = True) -> List[str]:
        """
        Returns the ancestors of Biolink categories, predicates, category mixins, or predicate mixins. Input
        categories/predicates/mixins are themselves included in the returned ancestor list. For categories/predicates,
        inclusion of mixin ancestors can be turned on or off via the include_mixins flag. Inclusion of ARAX-defined
        conflations (e.g., gene == protein) can be controlled via the include_conflations parameter.
        """
        # TODO: Make the include_mixins work for mixin inputs? (return only categories/predicates)
        input_item_set = self._convert_to_set(biolink_items)
        categories = input_item_set.intersection(set(self.biolink_lookup_map["categories"]))
        predicates = input_item_set.intersection(set(self.biolink_lookup_map["predicates"]))
        category_mixins = input_item_set.intersection(set(self.biolink_lookup_map["category_mixins"]))
        predicate_mixins = input_item_set.intersection(set(self.biolink_lookup_map["predicate_mixins"]))
        ancestors = input_item_set.copy()
        ancestor_property = "ancestors_with_mixins" if include_mixins else "ancestors"
        if include_conflations:
            categories = set(self.add_conflations(categories))
        for category in categories:
            ancestors.update(self.biolink_lookup_map["categories"][category][ancestor_property])
        for predicate in predicates:
            ancestors.update(self.biolink_lookup_map["predicates"][predicate][ancestor_property])
        for category_mixin in category_mixins:
            ancestors.update(self.biolink_lookup_map["category_mixins"][category_mixin]["ancestors"])
        for predicate_mixin in predicate_mixins:
            ancestors.update(self.biolink_lookup_map["predicate_mixins"][predicate_mixin]["ancestors"])
        return list(ancestors)

    def get_descendants(self, biolink_items: Union[str, List[str], Set[str]], include_mixins: bool = True, include_conflations: bool = True) -> List[str]:
        """
        Returns the descendants of Biolink categories, predicates, category mixins, or predicate mixins. Input
        categories/predicates/mixins are themselves included in the returned descendant list. For categories/predicates,
        inclusion of mixin descendants can be turned on or off via the include_mixins flag. Inclusion of ARAX-defined
        conflations (e.g., gene == protein) can be controlled via the include_conflations parameter.
        """
        # TODO: Make the include_mixins work for mixin inputs? (return only categories/predicates)
        input_item_set = self._convert_to_set(biolink_items)
        categories = input_item_set.intersection(set(self.biolink_lookup_map["categories"]))
        predicates = input_item_set.intersection(set(self.biolink_lookup_map["predicates"]))
        category_mixins = input_item_set.intersection(set(self.biolink_lookup_map["category_mixins"]))
        predicate_mixins = input_item_set.intersection(set(self.biolink_lookup_map["predicate_mixins"]))
        descendants = input_item_set.copy()
        descendant_property = "descendants_with_mixins" if include_mixins else "descendants"
        if include_conflations:
            categories = set(self.add_conflations(categories))
        for category in categories:
            descendants.update(self.biolink_lookup_map["categories"][category][descendant_property])
        for predicate in predicates:
            descendants.update(self.biolink_lookup_map["predicates"][predicate][descendant_property])
        for category_mixin in category_mixins:
            descendants.update(self.biolink_lookup_map["category_mixins"][category_mixin]["descendants"])
        for predicate_mixin in predicate_mixins:
            descendants.update(self.biolink_lookup_map["predicate_mixins"][predicate_mixin]["descendants"])
        return list(descendants)

    def get_canonical_predicates(self, predicates: Union[str, List[str], Set[str]]) -> List[str]:
        """
        Returns the canonical version of the input predicate(s). Accepts a single predicate or multiple predicates as
        input and always returns the canonical predicate(s) in a list.
        """
        # TODO: Add canonical predicates for predicate mixins?
        input_predicate_set = self._convert_to_set(predicates)
        valid_predicates = input_predicate_set.intersection(self.biolink_lookup_map["predicates"])
        invalid_predicates = input_predicate_set.difference(valid_predicates)
        if invalid_predicates:
            print(f"WARNING: Provided predicate(s) {invalid_predicates} do not exist in Biolink {self.biolink_version}")
        canonical_predicates = {self.biolink_lookup_map["predicates"][predicate]["canonical_predicate"]
                                for predicate in valid_predicates}
        canonical_predicates.update(invalid_predicates)  # Go ahead and include those we don't have canonical info for
        return list(canonical_predicates)

    def is_symmetric(self, predicate: str) -> bool:
        if predicate in self.biolink_lookup_map["predicates"]:
            return self.biolink_lookup_map["predicates"][predicate]["is_symmetric"]
        else:
            return True  # Consider unrecognized predicates symmetric (rather than throw error)

    def replace_mixins_with_direct_mappings(self, biolink_items: Union[str, List[str], Set[str]]) -> List[str]:
        input_item_set = self._convert_to_set(biolink_items)
        category_mixins = input_item_set.intersection(set(self.biolink_lookup_map["category_mixins"]))
        predicate_mixins = input_item_set.intersection(set(self.biolink_lookup_map["predicate_mixins"]))
        non_mixins = input_item_set.difference(category_mixins).difference(predicate_mixins)
        mixin_direct_mappings = set()
        for category_mixin in category_mixins:
            raw_direct_mappings = self.biolink_lookup_map["category_mixins"][category_mixin]["direct_mappings"]
            mixin_direct_mappings.update(raw_direct_mappings.intersection(self.biolink_lookup_map["categories"]))
        for predicate_mixin in predicate_mixins:
            raw_direct_mappings = self.biolink_lookup_map["predicate_mixins"][predicate_mixin]["direct_mappings"]
            mixin_direct_mappings.update(raw_direct_mappings.intersection(self.biolink_lookup_map["predicates"]))
        return list(non_mixins.union(mixin_direct_mappings))

    def filter_out_mixins(self, biolink_items: Union[List[str], Set[str]]) -> List[str]:
        """
        Removes any predicate or category mixins in the input list.
        """
        input_item_set = self._convert_to_set(biolink_items)
        all_predicate_mixins = set(self.biolink_lookup_map["predicate_mixins"])
        all_category_mixins = set(self.biolink_lookup_map["category_mixins"])
        non_mixin_items = input_item_set.difference(all_predicate_mixins).difference(all_category_mixins)
        return list(non_mixin_items)

    def add_conflations(self, categories: Union[str, List[str], Set[str]]) -> List[str]:
        """
        Adds any "equivalent" categories (according to ARAX) to the input categories.
        """
        category_set = self._convert_to_set(categories)
        return list({conflated_category for category in category_set
                     for conflated_category in self.arax_conflations.get(category, {category})})

    def get_root_category(self) -> str:
        return self.root_category

    def get_root_predicate(self) -> str:
        return self.root_predicate

    @staticmethod
    def get_current_arax_biolink_version() -> str:
        """
        Returns the current Biolink version that the ARAX system is using, according to the OpenAPI YAML file.
        """
        code_dir = f"{os.path.dirname(os.path.abspath(__file__))}/../.."
        openapi_yaml_path = f"{code_dir}/UI/OpenAPI/python-flask-server/openapi_server/openapi/openapi.yaml"
        with open(openapi_yaml_path) as api_file:
            openapi_yaml = yaml.safe_load(api_file)
        biolink_version = openapi_yaml["info"]["x-translator"]["biolink-version"]
        return biolink_version

    # ------------------------------------- Internal methods -------------------------------------------------- #

    def _load_biolink_lookup_map(self):
        lookup_map_file = pathlib.Path(self.biolink_lookup_map_path)
        if not lookup_map_file.exists():
            # Parse the relevant Biolink yaml file and create/save local indexes
            return self._create_biolink_lookup_map()
        else:
            # A local file already exists for this Biolink version, so just load it
            with open(self.biolink_lookup_map_path, "rb") as biolink_map_file:
                biolink_lookup_map = pickle.load(biolink_map_file)
            return biolink_lookup_map

    def _create_biolink_lookup_map(self) -> Dict[str, Dict[str, Dict[str, Union[str, List[str], bool]]]]:
        print(f"INFO: Building local Biolink {self.biolink_version} ancestor/descendant lookup map because one "
              f"doesn't yet exist")
        biolink_lookup_map = {"predicates": dict(), "categories": dict(),
                              "predicate_mixins": dict(), "category_mixins": dict()}
        # Grab the relevant Biolink yaml file
        response = requests.get(f"https://raw.githubusercontent.com/biolink/biolink-model/{self.biolink_version}/biolink-model.yaml",
                                timeout=10)
        if response.status_code != 200:  # Sometimes Biolink's tags start with 'v', so try that
            response = requests.get(f"https://raw.githubusercontent.com/biolink/biolink-model/v{self.biolink_version}/biolink-model.yaml",
                                    timeout=10)

        if response.status_code == 200:
            # Build predicate, category, and mixin trees from the Biolink yaml
            biolink_model = yaml.safe_load(response.text)
            predicate_tree, canonical_predicate_map, predicate_to_mixins_map, \
                predicate_mixin_tree, symmetric_predicates = self._build_predicate_trees(biolink_model)
            mixin_to_predicates_map = self._reverse_map(predicate_to_mixins_map)
            category_tree, category_to_mixins_map, category_mixin_tree = self._build_category_trees(biolink_model)
            mixin_to_categories_map = self._reverse_map(category_to_mixins_map)

            # Then flatmap all info we need (for mixins, predicates, and categories) for easy access
            for predicate_mixin_node in predicate_mixin_tree.all_nodes():
                predicate_mixin = predicate_mixin_node.identifier
                ancestors = self._get_ancestors_from_tree(predicate_mixin, predicate_mixin_tree)
                biolink_lookup_map["predicate_mixins"][predicate_mixin] = {
                    "ancestors": ancestors.difference({"MIXIN"}),  # Our made-up root doesn't count as an ancestor
                    "descendants": self._get_descendants_from_tree(predicate_mixin, predicate_mixin_tree),
                    "direct_mappings": mixin_to_predicates_map.get(predicate_mixin, set())
                }
            del biolink_lookup_map["predicate_mixins"]["MIXIN"]  # No longer need this imaginary root node
            for category_mixin_node in category_mixin_tree.all_nodes():
                category_mixin = category_mixin_node.identifier
                ancestors = self._get_ancestors_from_tree(category_mixin, category_mixin_tree)
                biolink_lookup_map["category_mixins"][category_mixin] = {
                    "ancestors": ancestors.difference({"MIXIN"}),  # Our made-up root doesn't count as an ancestor
                    "descendants": self._get_descendants_from_tree(category_mixin, category_mixin_tree),
                    "direct_mappings": mixin_to_categories_map.get(category_mixin, set())
                }
            del biolink_lookup_map["category_mixins"]["MIXIN"]  # No longer need this imaginary root node
            predicate_mixins_in_tree = {mixin_node.identifier for mixin_node in predicate_mixin_tree.all_nodes()}
            for predicate_node in predicate_tree.all_nodes():
                predicate = predicate_node.identifier
                ancestors = self._get_ancestors_from_tree(predicate, predicate_tree)
                descendants = self._get_descendants_from_tree(predicate, predicate_tree)
                mixin_ancestors = {mixin_ancestor for ancestor in ancestors
                                   for mixin in predicate_to_mixins_map[ancestor].intersection(predicate_mixins_in_tree)  # Skip weird mixins that have non-mixin parents
                                   for mixin_ancestor in biolink_lookup_map["predicate_mixins"][mixin]["ancestors"]}
                mixin_descendants = {mixin_descendant for descendant in descendants
                                     for mixin in predicate_to_mixins_map[descendant].intersection(predicate_mixins_in_tree)  # Skip weird mixins that have non-mixin parents
                                     for mixin_descendant in biolink_lookup_map["predicate_mixins"][mixin]["descendants"]}
                biolink_lookup_map["predicates"][predicate] = {
                    "ancestors": ancestors,
                    "descendants": descendants,
                    "ancestors_with_mixins": ancestors.union(mixin_ancestors),
                    "descendants_with_mixins": descendants.union(mixin_descendants),
                    "canonical_predicate": canonical_predicate_map.get(predicate, predicate),
                    "direct_mixins": predicate_to_mixins_map[predicate],
                    "is_symmetric": predicate in symmetric_predicates
                }
            for category_node in category_tree.all_nodes():
                category = category_node.identifier
                ancestors = self._get_ancestors_from_tree(category, category_tree)
                descendants = self._get_descendants_from_tree(category, category_tree)
                mixin_ancestors = {mixin_ancestor for ancestor in ancestors
                                   for mixin in category_to_mixins_map[ancestor]
                                   for mixin_ancestor in biolink_lookup_map["category_mixins"][mixin]["ancestors"]}
                mixin_descendants = {mixin_descendant for descendant in descendants
                                     for mixin in category_to_mixins_map[descendant]
                                     for mixin_descendant in biolink_lookup_map["category_mixins"][mixin]["descendants"]}
                biolink_lookup_map["categories"][category] = {
                    "ancestors": ancestors,
                    "descendants": descendants,
                    "ancestors_with_mixins": ancestors.union(mixin_ancestors),
                    "descendants_with_mixins": descendants.union(mixin_descendants),
                    "direct_mixins": category_to_mixins_map[category],
                }

            # And cache it (never needs to be refreshed for the given Biolink version)
            with open(self.biolink_lookup_map_path, "wb") as output_file:
                pickle.dump(biolink_lookup_map, output_file)  # Use pickle so we can save Sets
            # Also save a JSON version to help with debugging
            json_file_path = self.biolink_lookup_map_path.replace(".pickle", ".json")
            with open(json_file_path, "w+") as output_json_file:
                json.dump(biolink_lookup_map, output_json_file, default=self._serialize_with_sets, indent=4)
        else:
            raise RuntimeError(f"ERROR: Request to get Biolink {self.biolink_version} YAML file returned "
                               f"{response.status_code} response. Cannot load BiolinkHelper.")

        return biolink_lookup_map

    def _build_predicate_trees(self, biolink_model: dict) -> Tuple[Tree, Dict[str, str], Dict[str, Set[str]], Tree, Set[str]]:
        root_mixin = "MIXIN"  # This is made up for easier parsing

        # Build helper maps for predicates and their mixins
        parent_to_child_dict = defaultdict(set)
        canonical_predicate_map = dict()
        predicate_to_mixins_map = dict()
        symmetric_predicates = set()
        for slot_name_english, info in biolink_model["slots"].items():
            slot_name = self._convert_english_predicate_to_trapi_format(slot_name_english)
            # Record this node underneath its parent
            parent_name_english = info.get("is_a")
            if parent_name_english:
                parent_name = self._convert_english_predicate_to_trapi_format(parent_name_english)
                parent_to_child_dict[parent_name].add(slot_name)
            # Or if it's a top-level mixin, force it to have the (made-up) root mixin as parent
            elif info.get("mixin"):
                parent_to_child_dict[root_mixin].add(slot_name)
            # Record this node's direct mixins
            mixins_english = info.get("mixins", [])
            mixins = {self._convert_english_predicate_to_trapi_format(mixin_english) for mixin_english in mixins_english}
            predicate_to_mixins_map[slot_name] = mixins
            # Record whether this predicate is symmetric
            if info.get("symmetric"):
                symmetric_predicates.add(slot_name)
            # Record the canonical form of this predicate
            inverse_predicate_english = info.get("inverse")
            is_canonical_predicate = info.get("annotations", dict()).get("canonical_predicate")
            # A couple 'inverse' pairs of predicates in Biolink 3.0.3 seem to be missing a 'canonical_predicate' label,
            # so we work around that below (see https://github.com/biolink/biolink-model/issues/1112)
            canonical_predicate_english = slot_name_english if is_canonical_predicate or not inverse_predicate_english else inverse_predicate_english
            canonical_predicate = self._convert_english_predicate_to_trapi_format(canonical_predicate_english)
            canonical_predicate_map[slot_name] = canonical_predicate

        # Recursively build the predicates trees starting with the root
        predicate_tree = Tree()
        predicate_tree.create_node(self.root_predicate, self.root_predicate)
        self._create_tree_recursive(self.root_predicate, parent_to_child_dict, predicate_tree)
        predicate_mixin_tree = Tree()
        predicate_mixin_tree.create_node(root_mixin, root_mixin)
        self._create_tree_recursive(root_mixin, parent_to_child_dict, predicate_mixin_tree)

        return predicate_tree, canonical_predicate_map, predicate_to_mixins_map, predicate_mixin_tree, symmetric_predicates

    def _build_category_trees(self, biolink_model: dict) -> Tuple[Tree, Dict[str, Set[str]], Tree]:
        root_mixin = "MIXIN"  # This is made up for easier parsing

        # Build helper maps for categories and their mixins
        parent_to_child_dict = defaultdict(set)
        category_to_mixins_map = dict()
        for class_name_english, info in biolink_model["classes"].items():
            class_name = self._convert_english_category_to_trapi_format(class_name_english)
            # Record this node underneath its parent
            parent_name_english = info.get("is_a")
            ignore_is_a_slot = class_name == "biolink:ExposureEvent"  # See https://github.com/biolink/biolink-model/issues/1111; for now we'll consider ExposureEvent a mixin, and NOT is_a NamedThing
            if parent_name_english and not ignore_is_a_slot:
                parent_name = self._convert_english_category_to_trapi_format(parent_name_english)
                parent_to_child_dict[parent_name].add(class_name)
            # Or if it's a top-level mixin, force it to have the (made-up) root mixin as parent
            elif info.get("mixin"):
                parent_to_child_dict[root_mixin].add(class_name)
            # Record this node's direct mixins
            mixins_english = info.get("mixins", [])
            mixins = {self._convert_english_category_to_trapi_format(mixin_english) for mixin_english in mixins_english}
            category_to_mixins_map[class_name] = mixins

        # Recursively build the category trees starting with the root
        category_tree = Tree()
        category_tree.create_node(self.root_category, self.root_category)
        self._create_tree_recursive(self.root_category, parent_to_child_dict, category_tree)
        category_mixin_tree = Tree()
        category_mixin_tree.create_node(root_mixin, root_mixin)
        self._create_tree_recursive(root_mixin, parent_to_child_dict, category_mixin_tree)

        return category_tree, category_to_mixins_map, category_mixin_tree

    def _create_tree_recursive(self, root_id: str, parent_to_child_map: defaultdict, tree: Tree):
        for child_id in parent_to_child_map.get(root_id, []):
            tree.create_node(child_id, child_id, parent=root_id)
            self._create_tree_recursive(child_id, parent_to_child_map, tree)

    @staticmethod
    def _get_ancestors_from_tree(node_identifier: str, tree: Tree) -> Set[str]:
        ancestors = {node_identifier for node_identifier in tree.rsearch(node_identifier)}
        return ancestors

    @staticmethod
    def _get_descendants_from_tree(node_identifier: str, tree: Tree) -> Set[str]:
        sub_tree = tree.subtree(node_identifier)
        descendants = {node.identifier for node in sub_tree.all_nodes()}
        return descendants

    @staticmethod
    def _convert_english_predicate_to_trapi_format(english_predicate: str):
        return f"biolink:{english_predicate.replace(' ', '_')}"

    @staticmethod
    def _convert_english_category_to_trapi_format(english_category: str):
        camel_case_class_name = "".join([f"{word[0].upper()}{word[1:]}" for word in english_category.split(" ")])
        return f"biolink:{camel_case_class_name}"

    @staticmethod
    def _convert_to_set(items: any) -> Set[str]:
        if isinstance(items, str):
            return {items}
        elif isinstance(items, list):
            return set(items)
        elif isinstance(items, set):
            return items
        else:
            return set()

    @staticmethod
    def _serialize_with_sets(obj: any) -> any:
        return list(obj) if isinstance(obj, set) else obj

    @staticmethod
    def _reverse_map(str_to_set_map: Dict[str, Set[str]]) -> Dict[str, Set[str]]:
        reversed_map = defaultdict(set)
        for key, value_set in str_to_set_map.items():
            for value in value_set:
                reversed_map[value].add(key)
        return dict(reversed_map)


def main():
    arg_parser = argparse.ArgumentParser()
    arg_parser.add_argument('version', nargs='?', help="The Biolink Model version number to use")
    args = arg_parser.parse_args()

    bh = BiolinkHelper(biolink_version=args.version)

    # Test descendants
    chemical_entity_descendants = bh.get_descendants("biolink:ChemicalEntity", include_mixins=True)
    assert "biolink:Drug" in chemical_entity_descendants
    assert "biolink:PhysicalEssence" in chemical_entity_descendants
    assert "biolink:NamedThing" not in chemical_entity_descendants
    chemical_entity_descenants_no_mixins = bh.get_descendants("biolink:ChemicalEntity", include_mixins=False)
    assert "biolink:Drug" in chemical_entity_descenants_no_mixins
    assert "biolink:PhysicalEssence" not in chemical_entity_descenants_no_mixins
    assert "biolink:NamedThing" not in chemical_entity_descenants_no_mixins

    # Test ancestors
    protein_ancestors = bh.get_ancestors("biolink:Protein", include_mixins=True)
    assert "biolink:NamedThing" in protein_ancestors
    assert "biolink:ProteinIsoform" not in protein_ancestors
    protein_ancestors_no_mixins = bh.get_ancestors("biolink:Protein", include_mixins=False)
    assert "biolink:NamedThing" in protein_ancestors_no_mixins
    assert "biolink:ProteinIsoform" not in protein_ancestors_no_mixins
    assert len(protein_ancestors_no_mixins) < len(protein_ancestors)

    # Test predicates
    treats_ancestors = bh.get_ancestors("biolink:treats")
    assert "biolink:related_to" in treats_ancestors
    affects_descendants = bh.get_descendants("biolink:affects")
    assert "biolink:treats" in affects_descendants

    # Test lists
    combined_ancestors = bh.get_ancestors(["biolink:Gene", "biolink:Drug"])
    assert "biolink:Drug" in combined_ancestors
    assert "biolink:Gene" in combined_ancestors
    assert "biolink:BiologicalEntity" in combined_ancestors

    # Test conflations
    protein_ancestors = bh.get_ancestors("biolink:Protein", include_conflations=True)
    assert "biolink:Gene" in protein_ancestors
    gene_descendants = bh.get_descendants("biolink:Gene", include_conflations=True)
    assert "biolink:Protein" in gene_descendants
    gene_conflations = bh.add_conflations("biolink:Gene")
    assert set(gene_conflations) == {"biolink:Gene", "biolink:Protein"}

    # Test canonical predicates
    canonical_treated_by = bh.get_canonical_predicates("biolink:treated_by")
    canonical_treats = bh.get_canonical_predicates("biolink:treats")
    assert canonical_treated_by == ["biolink:treats"]
    assert canonical_treats == ["biolink:treats"]

    # Test filtering out mixins
    mixin_less_list = bh.filter_out_mixins(["biolink:Protein", "biolink:Drug", "biolink:PhysicalEssence"])
    assert set(mixin_less_list) == {"biolink:Protein", "biolink:Drug"}

    # Test replacing mixins with regular items (direct mappings)
    regular_list = bh.replace_mixins_with_direct_mappings(["biolink:Disease", "biolink:PhysicalEssence"])
    assert "biolink:Disease" in regular_list
    assert "biolink:PhysicalEssence" not in regular_list
    assert "biolink:ChemicalEntity" in regular_list
    filtered_out_mixins = bh.filter_out_mixins(regular_list)
    assert set(regular_list) == set(filtered_out_mixins)

    # Test predicate symmetry
    assert bh.is_symmetric("biolink:related_to")
    assert not bh.is_symmetric("biolink:treats")

    # Test getting biolink version
    biolink_version = bh.get_current_arax_biolink_version()
    assert biolink_version >= "2.1.0"

    print("All BiolinkHelper tests passed!")


if __name__ == "__main__":
    main()


# code/ARAX/Documentation/document_dsl_commands.py

import sys
import os
import importlib
from tomark import Tomark
import re
import md_toc
sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
modules = ["ARAX_messenger", "ARAX_expander", "ARAX_overlay", "ARAX_filter_kg", "ARAX_filter_results", "ARAX_resultify", "ARAX_ranker", "ARAX_connect", "ARAX_infer"]
classes = ["ARAXMessenger", "ARAXExpander", "ARAXOverlay", "ARAXFilterKG", "ARAXFilterResults", "ARAXResultify", "ARAXRanker", "ARAXConnect", "ARAXInfer"]
modules_to_command_name = {'ARAX_resultify': '`resultify()`', 'ARAX_messenger': '`create_message()`',
                           'ARAX_overlay': '`overlay()`', 'ARAX_filter_kg': '`filter_kg()`','ARAX_filter_results': '`filter_results()`', 
                           'ARAX_expander': '`expand()`', 'ARAX_ranker': '`rank_results()`', 'ARAX_connect':'`connect()`', 'ARAX_infer': '`infer()`'}
to_print = ""
header_info = """
# Domain Specific Langauage (DSL) description
This document describes the features and components of the DSL developed for the ARA Expander team.

Full documentation is given below, but an example can help: in the API specification, there is field called `Query.previous_message_processing_plan.processing_actions:`,
while initially an empty list, a set of processing actions can be applied with something along the lines of:

```
[
"add_qnode(name=hypertension, key=n00)",  # add a new node to the query graph
"add_qnode(category=biolink:Protein, is_set=True, key=n01)",  # add a new set of nodes of a certain type to the query graph
"add_qedge(subject=n01, object=n00, key=e00)",  # add an edge connecting these two nodes
"expand(edge_key=e00)",  # reach out to knowledge providers to find all subgraphs that satisfy these new query nodes/edges
"overlay(action=compute_ngd)",  # overlay each edge with the normalized Google distance (a metric based on Edge.subject and Edge.object co-occurrence frequency in all PubMed abstracts)
"filter_kg(action=remove_edges_by_attribute, edge_attribute=ngd, direction=above, threshold=0.85, remove_connected_nodes=t, qnode_key=n01)",  # remove all edges with normalized google distance above 0.85 as well as the connected protein
"return(message=true, store=false)"  # return the message to the ARS
]
```
 
# Full documentation of current DSL commands
"""
to_print += header_info
#for (module, cls) in zip(modules, classes):
#    m = importlib.import_module(module)
#    #print(getattr(m, cls)().describe_me())
#    dsl_name = modules_to_command_name[module]
#    to_print += f"# {module}\n"
#    to_print += f"|{dsl_name} DSL commands\n"
#    for dic in getattr(m, cls)().describe_me():
#        #to_print += Tomark.table([dic])
#        #to_print += "\n"
#        temp_table = Tomark.table([dic])
#        temp_table_split = temp_table.split("\n")
#        better_table = f"|{dsl_name} DSL commands" + ('|' * temp_table_split[0].count('|')) + '\n'
#        better_table += temp_table_split[1] + '-----|\n'
#        better_table += '|**DSL parameters**' + temp_table_split[0] + "\n"
#        better_table += '|**DSL arguments**' + temp_table_split[2] + "\n"
#        to_print += better_table + '\n'

for (module, cls) in zip(modules, classes):
    m = importlib.import_module(module)
    dsl_name = modules_to_command_name[module]
    to_print += f"## {module}\n"
    for dic in getattr(m, cls)().describe_me():
        if 'action' in dic:  # for classes that use the `action=` paradigm
            action = dic['action'].pop()
            del dic['action']
            to_print += '### ' + re.sub(r'\(\)',f'(action={action})', dsl_name) + '\n'
        elif 'dsl_command' in dic:  # for classes like ARAX_messenger that have different DSL commands with different top level names as methods to the main class
            dsl_command = dic['dsl_command']
            del dic['dsl_command']
            #to_print += '### ' + re.sub('\(\)', f'({subcommand}=)', dsl_name) + '\n'
            to_print += '### ' + dsl_command + '\n'
        else:  # for classes that don't use the `action=` paradigm like `expand()` and `resultify()`
            to_print += '### ' + dsl_name + '\n'
        if 'description' in dic:
            to_print += dic['description'] + '\n\n'
            del dic['description']
        if 'mutually_exclusive_params' in dic:
            if len(dic['mutually_exclusive_params']) < 3:
                mutual_string = '`' + '` and `'.join([str(x) for x in dic['mutually_exclusive_params']]) + '`'
            else:
                mutual_string = '`' + '`, `'.join([str(x) for x in dic['mutually_exclusive_params'][:-1]]) + '`, and `' + str(dic['mutually_exclusive_params'][-1]) + '`'
            to_print += "**NOTE:** The parameters " + mutual_string + ' are mutually exclusive and thus will cause an error when more than one is included.\n\n'
        if 'parameters' in dic:
            to_print += '#### parameters: ' + '\n\n'
            for param_key,param_val in dic['parameters'].items():
                to_print += '* ##### ' + param_key + '\n\n'
                if 'description' in param_val:
                    to_print += '    - ' + param_val['description'] + '\n\n'
                if 'type' in param_val:
                    to_print += '    - Acceptable input types: ' + param_val['type'] + '.\n\n'
                if 'depends_on' in param_val:
                    to_print += '    - *NOTE*:  If this parameter is included then the parameter `' + param_val['depends_on'] + '` must also be included for it to function.\n\n'
                if 'is_required' in param_val:
                    if param_val['is_required']:
                        to_print += '    - This is a required parameter and must be included.\n\n'
                    else:
                        to_print += '    - This is not a required parameter and may be omitted.\n\n'
                if 'examples' in param_val:
                    if len(param_val['examples']) < 3:
                        example_string = '`' + '` and `'.join([str(x) for x in param_val['examples']]) + '`'
                    else:
                        example_string = '`' + '`, `'.join([str(x) for x in param_val['examples'][:-1]]) + '`, and `' + str(param_val['examples'][-1]) + '`'                 
                    to_print += '    - ' + example_string + ' are examples of valid inputs.\n\n'
                if 'enum' in param_val:
                    if len(param_val['enum']) < 3:
                        enum_string = '`' + '` and `'.join([str(x) for x in param_val['enum']]) + '`'
                    else:
                        enum_string = '`' + '`, `'.join([str(x) for x in param_val['enum'][:-1]]) + '`, and `' + str(param_val['enum'][-1]) + '`'                 
                    to_print += '    - ' + enum_string + ' are all possible valid inputs.\n\n'
                if 'max' in param_val and 'min' in param_val:
                    to_print += '    - The values for this parameter can range from a minimum value of ' +str(param_val['min'])+ ' to a maximum value of ' +str(param_val['max'])+ '.\n\n'
                if 'default' in param_val:
                    to_print += '    - If not specified the default input will be ' + str(param_val['default']) + '. \n\n'
        # if dic:  # if the dic is empty, then don't create a table
        #     temp_table = Tomark.table([dic])
        #     temp_table_split = temp_table.split("\n")
        #     #better_table = "|"+re.sub('\(\)',f'(action={action})', dsl_name) + ('|' * temp_table_split[0].count('|')) + '\n'
        #     better_table = ('|' * (temp_table_split[0].count('|')+1)) + '\n'
        #     better_table += temp_table_split[1] + '-----|\n'
        #     better_table += '|_DSL parameters_' + temp_table_split[0] + "\n"
        #     better_table += '|_DSL arguments_' + temp_table_split[2] + "\n"
        #     to_print += better_table + '\n'
        else:
            to_print += '\n'

file_name = 'DSL_Documentation.md'
fid = open(file_name, 'w')
fid.write(to_print)
fid.close()

with_toc = "# Table of contents\n\n"
with_toc += md_toc.build_toc(os.path.dirname(os.path.abspath(__file__)) + '/' + file_name)
with_toc += to_print
fid = open(file_name, 'w')
fid.write(with_toc)
fid.close()



# code/ARAX/Examples/kg2_api_example.py

#!/usr/bin/env python3

import json
import pprint
import requests

with open("kg2_api_example.json", "r") as input_file:
    trapi_message = json.load(input_file)
    result = requests.post("https://arax.ncats.io/api/rtxkg2/v1.0/query?bypass_cache=false",
                           json=trapi_message)
    pprint.pprint(result.json())


# code/ARAX/KnowledgeSources/Generate_all_KP_apis.py

import sys
import os
import importlib

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../reasoningtool/kg-construction/")
mypath = os.path.dirname(os.path.abspath(__file__))+"/../../reasoningtool/kg-construction/"
onlyfiles = [f for f in os.listdir(mypath) if os.path.isfile(os.path.join(mypath, f))]
not_parsed = 0
parsed = 0
fid = open('API_LIST.yaml', 'w')
fid.write("""# Example list of KP API's ARAX has the ability to automatically utilize
# Each API provider should add one entry under APIs
# NOTE: THIS IS A VERY INCOMPLETE LIST OF ALL THE KP'S ARAX/RTX IS USING ONLY FOR KG1 (eg. only 12 of 28+ are listed here)
# "metadata" points to the API's openapi metadata
#            a relative path in this repo, or a full URL
# "translator" is a placeholder for translator specific attributes
# "parser" is a relative path indicating where the ARAX parser resides
# "name" is the name of the knowledge provider or source (mainly for internal use or human discussion)
# "functionalities" is a dictionary of endpoint functionality that this KP can provide
APIs:\n""")
for file in onlyfiles:
    if file[0:5] == "Query":
        try:
            module_name = file.split('.')[0]
            m = importlib.import_module(file.split('.')[0])
            metadata = getattr(m, module_name)().API_BASE_URL
            functionalities = getattr(m, module_name)().HANDLER_MAP
            fid.write(f"""  - name: {module_name[5:]}\n""")
            fid.write(f"""    translator:\n          - returnjson: true\n          - metadata: {metadata}\n""")
            fid.write("""    ARAXInfo:\n""")
            fid.write(f"""          - parser: ../../reasoningtool/kg-construction/{module_name}.py\n""")
            fid.write(f"""          - functionalities: {functionalities}\n""")
            fid.write(f"""          - name: {module_name[5:]}\n\n""")
            parsed += 1
        except:
            not_parsed += 1
#print(not_parsed)
#print(parsed)
fid.close()


#import QueryBioLink
#q = QueryBioLink.QueryBioLink()
#print(q.API_BASE_URL)
#print(q.HANDLER_MAP)

# code/ARAX/KnowledgeSources/create_csv_of_kp_node_pairs.py

#!/bin/env python3
"""create_csv_of_kp_node_pairs.py

Creates a CSV of all pairs of node types for KG1, KG2, and BTE (ARAX's current knowledge providers).
Resulting columns are: subject_type, object_type, provider, source, predicates

Usage: python create_csv_of_kp_node_pairs.py

"""

import requests
import sys
import os
import csv
import time

from neo4j import GraphDatabase

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../")  # code directory
from RTXConfiguration import RTXConfiguration


def get_bte_node_pairs():
    print("Grabbing BTE node pairs...")
    url = "https://smart-api.info/registry/translator/meta-kg"
    r = requests.get(url)
    print("...done.")
    bte_associations = r.json()['associations']
    node_pairs_dict = dict()
    for bte_association in bte_associations:
        subject_type = convert_pascal_case_to_snake_case(bte_association['subject']['semantic_type'])
        object_type = convert_pascal_case_to_snake_case(bte_association['object']['semantic_type'])
        predicate = bte_association['predicate']['label']
        provider = "BTE"
        source = bte_association['predicate']['source']
        node_pairs_dict = update_node_pairs_dict(node_pairs_dict, subject_type, object_type, predicate, provider, source)
    return list(node_pairs_dict.values())


def get_kg1_node_pairs():
    cypher = "match (n0)-[e]->(n1) return distinct n0.category, n1.category, e.predicate, e.provided_by"
    results = run_neo4j_query(cypher, "KG1")
    node_pairs_dict = dict()
    for result in results:
        subject_type = result.get('n0.category')
        object_type = result.get('n1.category')
        predicate = result.get('e.predicate')
        provider = "KG1"
        source = result.get('e.provided_by')
        node_pairs_dict = update_node_pairs_dict(node_pairs_dict, subject_type, object_type, predicate, provider, source)
    return list(node_pairs_dict.values())


def get_kg2_node_pairs():
    cypher = "match (n0)-[e]->(n1) return distinct n0.category_label, n1.category_label, e.simplified_edge_label, e.provided_by"
    results = run_neo4j_query(cypher, "KG2")
    node_pairs_dict = dict()
    for result in results:
        subject_type = result.get('n0.category_label')
        object_type = result.get('n1.category_label')
        predicate = result.get('e.simplified_edge_label')
        provider = "KG2"
        # NOTE: Have to do some special handling for KG2 edge.provided_by being a list with buggy format
        sources = result.get('e.provided_by')
        for source in sources:
            node_pairs_dict = update_node_pairs_dict(node_pairs_dict, subject_type, object_type, predicate, provider, source)
    return list(node_pairs_dict.values())


def get_node_pair_key(source, subject_type, object_type):
    return f"{source}:{subject_type}->{object_type}"


def convert_pascal_case_to_snake_case(pascal_string):
    snake_string = pascal_string[0].lower()
    if len(pascal_string) > 1:
        for letter in pascal_string[1:]:
            if letter.isupper():
                snake_string += "_"
            snake_string += letter.lower()
    return snake_string


def update_node_pairs_dict(node_pairs_dict, subject_type, object_type, predicate, provider, source):
    node_pair_key = get_node_pair_key(source, subject_type, object_type)
    if node_pair_key in node_pairs_dict:
        node_pairs_dict[node_pair_key]['predicates'].add(predicate)
    else:
        node_pairs_dict[node_pair_key] = {'subject_type': subject_type, 'object_type': object_type,
                                          'provider': provider, 'source': source, 'predicates': {predicate}}
    return node_pairs_dict


def run_neo4j_query(cypher, kg_name):
    rtx_config = RTXConfiguration()
    rtx_config.neo4j_kg2 = "KG2pre"
    driver = GraphDatabase.driver(rtx_config.neo4j_bolt, auth=(rtx_config.neo4j_username, rtx_config.neo4j_password))
    with driver.session() as session:
        start = time.time()
        print(f"Grabbing node pairs from {kg_name} neo4j...")
        results = session.run(cypher).data()
        print(f"...done. Query took {round((time.time() - start) / 60, 2)} minutes.")
    driver.close()
    return results


def main():
    print("----- starting script -----")
    bte_node_pairs = get_bte_node_pairs()
    kg1_node_pairs = get_kg1_node_pairs()
    kg2_node_pairs = get_kg2_node_pairs()
    all_pairs = bte_node_pairs + kg1_node_pairs + kg2_node_pairs
    keys = all_pairs[0].keys()
    with open('kp_node_pairs.csv', 'w') as output_file:
        dw = csv.DictWriter(output_file, keys)
        dw.writeheader()
        dw.writerows(all_pairs)
    print("----- script finished -----")


if __name__ == "__main__":
    main()


# code/ARAX/KnowledgeSources/create_csv_of_kp_predicate_triples.py

#!/bin/env python3
"""create_csv_of_kp_predicate_triples.py

Creates a CSV of all predicate triples of the form (node type, edge type, node type) for KG1, KG2, and BTE (ARAX's current knowledge providers).
Resulting columns are: subject_type, edge_type, object_type

Usage: python create_csv_of_kp_predicate_triples.py

"""

# adapted from Amy Glen's code in create_csv_of_kp_node_pairs.py

import requests
import sys
import os
import csv
import time
import pandas as pd
import json

from neo4j import GraphDatabase

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../")  # code directory
from RTXConfiguration import RTXConfiguration


def run_neo4j_query(cypher, kg_name, data_type):
    rtx_config = RTXConfiguration()
    rtx_config.neo4j_kg2 = kg_name
    driver = GraphDatabase.driver(rtx_config.neo4j_bolt, auth=(rtx_config.neo4j_username, rtx_config.neo4j_password))
    with driver.session() as session:
        start = time.time()
        print(f"Grabbing {data_type} from {kg_name} neo4j...")
        results = session.run(cypher).data()
        print(f"...done. Query took {round((time.time() - start) / 60, 2)} minutes.")
    driver.close()
    return results


def get_kg2_predicate_triples():
    cypher = 'match (n)-[r]->(m) with distinct labels(n) as n1s, type(r) as rel, '+\
            'labels(m) as n2s unwind n1s as n1 unwind n2s as n2 with distinct n1 as '+\
            'node1, rel as relationship, n2 as node2 where node1 <> "Base" and '+\
            'node2 <> "Base" return node1, relationship, node2'
            # Changed this from using n.category so that it can handle node with multiple labels
            # Unfortunetly this makes the cypher a little more unweildly and likely slows a query a bit.
    results = run_neo4j_query(cypher, "KG2pre", "predicate triples")
    triples_dict = {"subject":[], "predicate":[], "object":[]}
    for result in results:
        subject_type = result.get('node1')
        object_type = result.get('node2')
        predicate = result.get('relationship')
        triples_dict['subject'].append(subject_type)
        triples_dict['object'].append(object_type)
        triples_dict['predicate'].append(predicate)
    return pd.DataFrame(triples_dict)

def get_kg2c_predicate_triples():
    cypher = 'match (n)-[r]->(m) with distinct labels(n) as n1s, type(r) as rel, '+\
            'labels(m) as n2s unwind n1s as n1 unwind n2s as n2 with distinct n1 as '+\
            'node1, rel as relationship, n2 as node2 where node1 <> "Base" and '+\
            'node2 <> "Base" return node1, relationship, node2'
            # Changed this from using n.category so that it can handle node with multiple labels
            # Unfortunetly this makes the cypher a little more unweildly and likely slows a query a bit.
    results = run_neo4j_query(cypher, "KG2c", "predicate triples")
    triples_dict = {"subject":[], "predicate":[], "object":[]}
    for result in results:
        subject_type = result.get('node1')
        object_type = result.get('node2')
        predicate = result.get('relationship')
        triples_dict['subject'].append(subject_type)
        triples_dict['object'].append(object_type)
        triples_dict['predicate'].append(predicate)
    return pd.DataFrame(triples_dict)

def get_kg2_predicate_triples_examples():
    cypher = 'match (n)-[r]->(m) with distinct labels(n) as n1s, type(r) as rel, labels(m) as n2s ' +\
                'unwind n1s as n1 unwind n2s as n2 with distinct n1 as node1, rel as relationship, ' +\
                'n2 as node2 where node1 <> "Base" and node2 <> "Base" CALL ' +\
                "apoc.cypher.run('match (n2:`' + node1 + '`)-[r2:`' + relationship + '`]->(m2:`' + node2 + '`) " +\
                "return n2.id as subject, m2.id as object limit 1', {}) YIELD value return node1, relationship, " +\
                "node2, value.subject as subject, value.object as object"
    results = run_neo4j_query(cypher, "KG2pre", "predicate triples")
    triples_dict = {"subject":[], "predicate":[], "object":[]}
    examples_json = {"url": "https://arax.ncats.io/api/rtxkg2/v1.1/query",
                    "TRAPI": True,
                    "edges": []}
    for result in results:
        subject_type = result.get('node1')
        object_type = result.get('node2')
        predicate = result.get('relationship')
        subject_example = result.get('subject')
        object_example = result.get('object')
        triples_dict['subject'].append(subject_type)
        triples_dict['object'].append(object_type)
        triples_dict['predicate'].append(predicate)
        example = {
            "subject_category": subject_type,
            "object_category": object_type,
            "predicate": predicate,
            "subject": subject_example,
            "object": object_example
        }
        examples_json['edges'].append(example)
    return pd.DataFrame(triples_dict).drop_duplicates(ignore_index=True), examples_json

def get_kg2c_predicate_triples_examples():
    cypher = 'match (n)-[r]->(m) with distinct labels(n) as n1s, type(r) as rel, labels(m) as n2s ' +\
                'unwind n1s as n1 unwind n2s as n2 with distinct n1 as node1, rel as relationship, ' +\
                'n2 as node2 where node1 <> "Base" and node2 <> "Base" CALL ' +\
                "apoc.cypher.run('match (n2:`' + node1 + '`)-[r2:`' + relationship + '`]->(m2:`' + node2 + '`) " +\
                "return n2.id as subject, m2.id as object limit 1', {}) YIELD value return node1, relationship, " +\
                "node2, value.subject as subject, value.object as object"
    results = run_neo4j_query(cypher, "KG2c", "predicate triples")
    triples_dict = {"subject":[], "predicate":[], "object":[]}
    examples_json = {"url": "https://kg2.transltr.io/api/rtxkg2/v1.2",
                    "TRAPI": True,
                    "edges": []}
    for result in results:
        subject_type = result.get('node1')
        object_type = result.get('node2')
        predicate = result.get('relationship')
        subject_example = result.get('subject')
        object_example = result.get('object')
        triples_dict['subject'].append(subject_type)
        triples_dict['object'].append(object_type)
        triples_dict['predicate'].append(predicate)
        example = {
            "subject_category": subject_type,
            "object_category": object_type,
            "predicate": predicate,
            "subject": subject_example,
            "object": object_example
        }
        examples_json['edges'].append(example)
    return pd.DataFrame(triples_dict).drop_duplicates(ignore_index=True), examples_json

def get_kg2_node_labels():
    cypher = 'call db.labels()'
    results = run_neo4j_query(cypher, "KG2pre", "node labels")
    labels_dict = {"label":[]}
    for result in results:
        label = result.get('label')
        labels_dict["label"].append(label)
    return pd.DataFrame(labels_dict)

def get_kg2c_node_labels():
    cypher = 'call db.labels()'
    results = run_neo4j_query(cypher, "KG2c", "node labels")
    labels_dict = {"label":[]}
    for result in results:
        label = result.get('label')
        labels_dict["label"].append(label)
    return pd.DataFrame(labels_dict)

def get_kg2_relationship_types():
    cypher = 'call db.relationshipTypes()'
    results = run_neo4j_query(cypher, "KG2pre", "relationship types")
    predicate_dict = {"predicate":[]}
    for result in results:
        predicate = result.get('relationshipType')
        predicate_dict["predicate"].append(predicate)
    return pd.DataFrame(predicate_dict)

def get_kg2c_relationship_types():
    cypher = 'call db.relationshipTypes()'
    results = run_neo4j_query(cypher, "KG2c", "relationship types")
    predicate_dict = {"predicate":[]}
    for result in results:
        predicate = result.get('relationshipType')
        predicate_dict["predicate"].append(predicate)
    return pd.DataFrame(predicate_dict)


def main():
    print("----- starting script -----")
    
    #kg2_triple_df, KG2_examples_json = get_kg2_predicate_triples_examples()
    kg2c_triple_df, KG2c_examples_json = get_kg2c_predicate_triples_examples()
    #kg2_triple_df.to_csv("KG2_allowed_predicate_triples.csv", index=False)
    #with open('RTX_KG2_Data.json','w') as fid:
    #    json.dump(KG2_examples_json, fid, indent=4)
    kg2c_triple_df.to_csv("KG2c_allowed_predicate_triples.csv", index=False)
    with open('RTX_KG2c_Data.json','w') as fid:
        json.dump(KG2c_examples_json, fid, indent=4)

    #kg2_labels_df = get_kg2_node_labels()
    kg2c_labels_df = get_kg2c_node_labels()
    #kg2_labels_df.to_csv("KG2_allowed_node_labels.csv", index=False)
    kg2c_labels_df.to_csv("KG2c_allowed_node_labels.csv", index=False)

    #kg2_labels_df = get_kg2_relationship_types()
    kg2c_labels_df = get_kg2c_relationship_types()
    #kg2_labels_df.to_csv("KG2_allowed_relationship_types.csv", index=False)
    kg2c_labels_df.to_csv("KG2c_allowed_relationship_types.csv", index=False)
    print("----- script finished -----")

if __name__ == "__main__":
    main()


# code/ARAX/KnowledgeSources/knowledge_source_metadata.py

#!/bin/env python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast
import re
import inspect
import csv

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
import Expand.expand_utilities as eu

pathlist = os.path.realpath(__file__).split(os.path.sep)
RTXindex = pathlist.index("RTX")
sys.path.append(os.path.sep.join([*pathlist[:(RTXindex + 1)], 'code']))

from RTXConfiguration import RTXConfiguration


class KnowledgeSourceMetadata:

    #### Define a class variable to cache the meta_knowledge_graph between objects
    cached_meta_knowledge_graph = None
    cached_simplified_meta_knowledge_graph = None

    #### Constructor
    def __init__(self):
        self.predicates = None
        self.meta_knowledge_graph = KnowledgeSourceMetadata.cached_meta_knowledge_graph
        self.simplified_meta_knowledge_graph = KnowledgeSourceMetadata.cached_simplified_meta_knowledge_graph
        self.RTXConfig = RTXConfiguration()


    #### Get a list of all supported subjects, predicates, and objects and reformat to /predicates format
    def get_kg_predicates(self):
        method_name = inspect.stack()[0][3]

        #### If we've already loaded the predicates, just return it
        if self.predicates is not None:
            return self.predicates

        # We always furnish KG2C results
        kg_prefix = 'KG2C'

        # Verify that the source data file exists
        input_filename = os.path.dirname(os.path.abspath(__file__)) + f"/{kg_prefix}_allowed_predicate_triples.csv"
        if not os.path.exists(input_filename):
            eprint(f"ERROR [{method_name}]: File '{input_filename}' not found")
            return None

        # Read the data and fill the predicates dict
        predicates = {}
        iline = 0
        with open(input_filename) as infile:
            rows = csv.reader(infile, delimiter=',', quotechar='"')
            for columns in rows:
                iline += 1

                # Ensure there are exactly 3 columns
                if len(columns) != 3:
                    eprint(f"ERROR [{method_name}]: input file {input_filename} line '{iline} does not have 3 columns")
                    continue

                subject_category = columns[0]
                predicate = columns[1]
                object_category = columns[2]

                if subject_category not in predicates:
                    predicates[subject_category] = {}

                if object_category not in predicates[subject_category]:
                    predicates[subject_category][object_category] = []

                predicates[subject_category][object_category].append(predicate)

        self.predicates = predicates

        return predicates


    #### Get a list of all supported subjects, predicates, and objects and return in /meta_knowledge_graph format
    def get_meta_knowledge_graph(self, format_='full'):
        method_name = inspect.stack()[0][3]

        #### If we've already loaded the meta_knowledge_graph, just return it
        if self.meta_knowledge_graph is not None:
            if format_ == 'simple':
                return self.simplified_meta_knowledge_graph
            else:
                return self.meta_knowledge_graph

        # We always furnish KG2C results
        kg_prefix = 'kg2c'

        # Verify that the source data file exists
        #input_filename = os.path.dirname(os.path.abspath(__file__)) + f"/{kg_prefix}_meta_kg.json"
        input_filename = f"{os.path.dirname(os.path.abspath(__file__))}/{self.RTXConfig.kg2c_meta_kg_path.split('/')[-1]}"
        if not os.path.exists(input_filename):
            eprint(f"ERROR [{method_name}]: File '{input_filename}' not found")
            return None

        try:
            with open(input_filename) as infile:
                self.meta_knowledge_graph = json.load(infile)
                KnowledgeSourceMetadata.cached_meta_knowledge_graph = self.meta_knowledge_graph
                self.create_simplified_meta_knowledge_graph()
                KnowledgeSourceMetadata.cached_simplified_meta_knowledge_graph = self.simplified_meta_knowledge_graph
                if format_ == 'simple':
                    return self.simplified_meta_knowledge_graph
                else:
                    return self.meta_knowledge_graph

        except:
            eprint(f"ERROR [{method_name}]: Unable to read meta_knowledge_graph from file '{input_filename}'")
            return


    #### Get a list of all supported subjects, predicates, and objects and return in /meta_knowledge_graph format
    def create_simplified_meta_knowledge_graph(self):
        method_name = inspect.stack()[0][3]

        self.simplified_meta_knowledge_graph = {
            'predicates_by_categories': {},
            'supported_predicates': {}
        }

        for edge in self.meta_knowledge_graph['edges']:
            if edge['subject'] not in self.simplified_meta_knowledge_graph['predicates_by_categories']:
                self.simplified_meta_knowledge_graph['predicates_by_categories'][edge['subject']] = {}
            if edge['object'] not in self.simplified_meta_knowledge_graph['predicates_by_categories'][edge['subject']]:
                self.simplified_meta_knowledge_graph['predicates_by_categories'][edge['subject']][edge['object']] = {}
            self.simplified_meta_knowledge_graph['predicates_by_categories'][edge['subject']][edge['object']][edge['predicate']] = True
            self.simplified_meta_knowledge_graph['supported_predicates'][edge['predicate']] = True

        for subject_category,subject_dict in self.simplified_meta_knowledge_graph['predicates_by_categories'].items():
            for object_category,object_dict in subject_dict.items():
                self.simplified_meta_knowledge_graph['predicates_by_categories'][subject_category][object_category] = sorted(list(object_dict))

        self.simplified_meta_knowledge_graph['supported_predicates'] = sorted(list(self.simplified_meta_knowledge_graph['supported_predicates']))


##########################################################################################
def main():

    ksm = KnowledgeSourceMetadata()
    #predicates = ksm.get_kg_predicates()
    #print(json.dumps(predicates,sort_keys=True,indent=2))

    meta_knowledge_graph = ksm.get_meta_knowledge_graph(format='simple')
    print(json.dumps(meta_knowledge_graph,sort_keys=True,indent=2))

if __name__ == "__main__": main()


# code/ARAX/ResponseCache/reinitialize_database.py

#!/bin/env python3
# Reinitialize the Message/Feedback MySQL database

import os
import sys
import json
import ast

from response_cache import ResponseCache

if len(sys.argv) < 2:
    print("DANGER! Run reinitialize_database.pl with the parameter 'yes' if you really want to delete and re-create the Response database")
    sys.exit(0)

if sys.argv[1] != 'yes':
    print("If you really want to delete and re-create the feedback/Message database, use:")
    print("  python reinitialize_database.py yes")
    sys.exit(0)

#### Create an RTX Feedback management object
response_cache = ResponseCache()

#### Purge and re-create the database
response_cache.create_database()
response_cache.prepopulate_database()

#### Connect to the database
response_cache.connect()



# code/ARAX/ResponseCache/response_cache.py

#!/usr/bin/python3
# Database definition and RTXFeedback class
from ARAX.ARAXQuery.ARAX_attribute_parser import ARAXAttributeParser
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import sys
import re
import json
import ast
from datetime import datetime
import pickle
import hashlib
import collections
import requests
import json
import requests
import requests_cache
from flask import Flask,redirect
import copy

import boto3
import timeit

import sqlalchemy
from sqlalchemy import Column, ForeignKey, Integer, Float, String, DateTime, Text, PickleType, LargeBinary
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import desc
from sqlalchemy import inspect

from reasoner_validator import validate
from jsonschema.exceptions import ValidationError

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../..")
from RTXConfiguration import RTXConfiguration
from ARAX_attribute_parser import ARAXAttributeParser

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../UI/OpenAPI/python-flask-server/")
from openapi_server.models.response import Response as Envelope

trapi_version = '1.3.0'


Base = declarative_base()

#### Define the database tables as classes
class Response(Base):
    __tablename__ = 'TRAPI_1_0_0_response'
    response_id = Column(Integer, primary_key=True)
    response_datetime = Column(DateTime, nullable=False)
    tool_version = Column(String(50), nullable=False)
    response_code = Column(String(50), nullable=False)
    message = Column(Text, nullable=False)
    n_results = Column(Integer, nullable=False)


#### The main ResponseCache class
class ResponseCache:

    #### Constructor
    def __init__(self):
        self.rtxConfig = RTXConfiguration()
        self.databaseName = "ResponseCache"
        self.engine_type = 'sqlite'
        if self.rtxConfig.is_production_server:
            self.engine_type = 'mysql'
        self.connect()

    #### Destructor
    def __del__(self):
        self.disconnect()


    #### Define attribute session
    @property
    def session(self) -> str:
        return self._session

    @session.setter
    def session(self, session: str):
        self._session = session


    #### Define attribute engine
    @property
    def engine(self) -> str:
        return self._engine

    @engine.setter
    def engine(self, engine: str):
        self._engine = engine


    #### Define attribute databaseName
    @property
    def databaseName(self) -> str:
        return self._databaseName

    @databaseName.setter
    def databaseName(self, databaseName: str):
        self._databaseName = databaseName


    ##################################################################################################
    #### Delete and create the ResponseStore database. Careful!
    def create_database(self):
        print("Creating database")

        # If the engine_type is mysql then set up the MySQL database
        if self.engine_type == 'mysql':
            engine = create_engine("mysql+pymysql://" + self.rtxConfig.mysql_feedback_username + ":" +
                self.rtxConfig.mysql_feedback_password + "@" + self.rtxConfig.mysql_feedback_host + "/" + self.databaseName)

        # Else just use SQLite
        else:
            database_path = os.path.dirname(os.path.abspath(__file__)) + '/' + self.databaseName + '.sqlite'
            if os.path.exists(database_path):
                os.remove(database_path)
            engine = create_engine("sqlite:///"+database_path)

        Base.metadata.drop_all(engine)
        Base.metadata.create_all(engine)
        self.connect()


    ##################################################################################################
    #### Create and store a database connection
    def connect(self):

        # If the engine_type is mysql then connect to the MySQL database
        if self.engine_type == 'mysql':
            engine = create_engine("mysql+pymysql://" + self.rtxConfig.mysql_feedback_username + ":" +
                self.rtxConfig.mysql_feedback_password + "@" + self.rtxConfig.mysql_feedback_host + "/" + self.databaseName)

        # Else just use SQLite
        else:
            database_path = os.path.dirname(os.path.abspath(__file__)) + '/' + self.databaseName + '.sqlite'
            engine = create_engine("sqlite:///"+database_path)

        DBSession = sessionmaker(bind=engine)
        session = DBSession()
        self.session = session
        self.engine = engine

        #### If the tables don't exist, then create the database
        database_info = sqlalchemy.inspect(engine)
        if not database_info.has_table(Response.__tablename__):
            eprint(f"WARNING: {self.engine_type} tables do not exist; creating them")
            Base.metadata.create_all(engine)


    ##################################################################################################
    #### Create and store a database connection
    def disconnect(self):
        session = self.session
        engine = self.engine
        session.close()
        try:
            engine.dispose()
        except:
            pass


    ##################################################################################################
    #### Pre-populate the database with reference data (none now)
    def prepopulate_database(self):
        session = self.session
        return


    ##################################################################################################
    #### Store a new response into the database
    def add_new_response(self,response):

        session = self.session
        envelope = response.envelope
        message = envelope.message

        response.debug(f"Writing response record to MySQL")
        try:
            stored_response = Response(response_datetime=datetime.now(),tool_version=self.rtxConfig.version,
                response_code=envelope.status,message=envelope.description,n_results=len(envelope.message.results))
            session.add(stored_response)
            session.flush()
            session.commit()
            response_id = stored_response.response_id
            response_filename = f"/responses/{response_id}.json"
        except:
            response.error(f"Unable to store response record in MySQL", error_code="InternalError")
            response_filename = f"/responses/error.json"
            response_id = 0

        servername = 'localhost'
        if self.rtxConfig.is_production_server:
            servername = 'arax.ncats.io'
        envelope.id = f"https://{servername}/api/arax/v1.2/response/{response_id}"

        #### New system to store the responses in an S3 bucket
        rtx_config = RTXConfiguration()
        KEY_ID = rtx_config.config_secrets['s3']['access']
        ACCESS_KEY = rtx_config.config_secrets['s3']['secret']
        succeeded_to_s3 = False

        try:
            response.debug(f"Writing response JSON to S3 bucket")
            s3 = boto3.resource(
                's3',
                region_name='us-west-2',
                aws_access_key_id=KEY_ID,
                aws_secret_access_key=ACCESS_KEY
            )
            serialized_query_graph = json.dumps(envelope.to_dict(), sort_keys=True, indent=2)
            t0 = timeit.default_timer()
            s3.Object('arax-response-storage', response_filename).put(Body=serialized_query_graph)
            t1 = timeit.default_timer()
            print("Elapsed time: "+str(t1-t0))
            response.debug(f"Wrote {response_filename} in {t1-t0} seconds")
            succeeded_to_s3 = True

        except:
            response.error(f"Unable to store response in S3 bucket as {response_filename}", error_code="InternalError")


        #### if the S3 write failed, store it as a JSON file on the filesystem
        if not succeeded_to_s3:
            response_dir = os.path.dirname(os.path.abspath(__file__)) + '/../../../data/responses_1_0'
            if not os.path.exists(response_dir):
                try:
                    os.mkdir(response_dir)
                except:
                    eprint(f"ERROR: Unable to create dir {response_dir}")

            if os.path.exists(response_dir):
                response_filename = f"{stored_response.response_id}.json"
                response_path = f"{response_dir}/{response_filename}"
                try:
                    with open(response_path, 'w') as outfile:
                        json.dump(envelope.to_dict(), outfile, sort_keys=True, indent=2)
                except:
                    eprint(f"ERROR: Unable to write response to file {response_path}")

        return stored_response.response_id


    ##################################################################################################
    #### Fetch a cached response
    def get_response(self, response_id):
        session = self.session

        if response_id is None:
            return( { "status": 400, "title": "response_id missing", "detail": "Required attribute response_id is missing from URL", "type": "about:blank" }, 400)

        response_id = str(response_id)

        #### Check to see if this is an integer. If so, it is a local response id
        match = re.match(r'\d+\s*$',response_id)
        if match:
            #### Find the response
            stored_response = session.query(Response).filter(Response.response_id==int(response_id)).first()
            if stored_response is not None:

                found_response_locally = False
                response_dir = os.path.dirname(os.path.abspath(__file__)) + '/../../../data/responses_1_0'
                response_filename = f"{stored_response.response_id}.json"
                response_path = f"{response_dir}/{response_filename}"
                try:
                    with open(response_path) as infile:
                        envelope = json.load(infile)
                    found_response_locally = True
                except:
                    eprint(f"ERROR: Unable to read response from file '{response_path}'. Will now try S3")

                #### If the file wasn't local, try it in S3
                if not found_response_locally:
                    rtx_config = RTXConfiguration()
                    KEY_ID = rtx_config.config_secrets['s3']['access']
                    ACCESS_KEY = rtx_config.config_secrets['s3']['secret']

                    try:
                        s3 = boto3.resource(
                            's3',
                            region_name='us-west-2',
                            aws_access_key_id=KEY_ID,
                            aws_secret_access_key=ACCESS_KEY
                        )

                        response_filename = f"/responses/{response_id}.json"
                        eprint(f"INFO: Attempting to read {response_filename} from S3")
                        t0 = timeit.default_timer()

                        content = s3.Object('arax-response-storage', response_filename).get()["Body"].read()
                        envelope = json.loads(content)
                        t1 = timeit.default_timer()
                        print("Elapsed time: "+str(t1-t0))
                        eprint(f"INFO: Successfully read {response_filename} from S3 in {t1-t0} seconds")

                    except:
                        eprint(f"ERROR: Unable to read {response_filename} from S3")
                        return( { "status": 404, "title": "Response not found", "detail": "There is no response corresponding to response_id="+str(response_id), "type": "about:blank" }, 404)


                #### Perform a validation on it
                try:
                    validate(envelope,'Response',trapi_version)
                    if 'description' not in envelope or envelope['description'] is None:
                        envelope['description'] = 'reasoner-validator: PASS'
                    envelope['validation_result'] = { 'status': 'PASS', 'version': trapi_version, 'message': '' }

                except ValidationError as error:
                    timestamp = str(datetime.now().isoformat())
                    if 'logs' not in envelope or envelope['logs'] is None:
                        envelope['logs'] = []
                    envelope['logs'].append( { "code": 'InvalidTRAPI', "level": "ERROR", "message": "TRAPI validator reported an error: " + str(error),
                        "timestamp": timestamp } )
                    if 'description' not in envelope or envelope['description'] is None:
                        envelope['description'] = ''
                    envelope['description'] = 'ERROR: TRAPI validator reported an error: ' + str(error) + ' --- ' + envelope['description']
                    envelope['validation_result'] = { 'status': 'FAIL', 'version': trapi_version, 'message': 'TRAPI validator reported an error: ' + str(error) + ' --- ' + envelope['description'] }

                #### Count provenance information
                attribute_parser = ARAXAttributeParser(envelope,envelope['message'])
                envelope['validation_result']['provenance_summary'] = attribute_parser.summarize_provenance_info()

                return envelope

            else:
                return( { "status": 404, "title": "Response not found", "detail": "There is no response corresponding to response_id="+str(response_id), "type": "about:blank" }, 404)

        #### Otherwise, see if it is an ARS style response_id
        if len(response_id) > 30:
            debug = False
            ars_hosts = [ 'ars-prod.transltr.io', 'ars.test.transltr.io', 'ars.ci.transltr.io', 'ars-dev.transltr.io', 'ars.transltr.io' ]
            for ars_host in ars_hosts:
                with requests_cache.disabled():
                    if debug:
                        eprint(f"Trying {ars_host}...")
                    try:
                        response_content = requests.get(f"https://{ars_host}/ars/api/messages/"+response_id, headers={'accept': 'application/json'})
                    except Exception as e:
                        return( { "status": 404, "title": f"Remote host {ars_host} unavailable", "detail": f"Connection attempts to {ars_host} triggered an exception: {e}", "type": "about:blank" }, 404)
                status_code = response_content.status_code
                if debug:
                    eprint(f"--- Fetch of {response_id} from {ars_host} yielded {status_code}")
                if status_code == 200:
                    if debug:
                        eprint(f"Got 200 from {ars_host}...")
                    break

            if status_code != 200:
                if debug:
                    eprint("Cannot fetch from ARS a response corresponding to response_id="+str(response_id))
                return( { "status": 404, "title": "Response not found", "detail": "Cannot fetch from ARS a response corresponding to response_id="+str(response_id), "type": "about:blank" }, 404)

            content_size = len(response_content.content)
            if content_size < 1000:
                content_size = '{:.2f} kB'.format(content_size/1000)
            elif content_size < 1000000:
                content_size = '{:.0f} kB'.format(content_size/1000)
            elif content_size < 10000000000:
                content_size = '{:.1f} MB'.format(content_size/1000000)
            else:
                content_size = '{:.0f} MB'.format(content_size/1000000)

            #### Unpack the response content into a dict
            try:
                response_dict = response_content.json()
            except:
                return( { "status": 404, "title": "Error decoding Response", "detail": "Cannot decode ARS response_id="+str(response_id)+" to a Translator Response", "type": "about:blank" }, 404)

            #### Debugging
            if debug:
                temp = copy.deepcopy(response_dict)
                temp['fields']['data'] = '...'
                eprint(json.dumps(temp,indent=2,sort_keys=True))

            is_parent_pk = False
            if 'fields' in response_dict:
                if 'name' in response_dict['fields'] and response_dict['fields']['name'] != '':
                    if response_dict['fields']['name'] == 'ars-default-agent' or response_dict['fields']['name'] == 'ars-workflow-agent':
                        is_parent_pk = True
                    else:
                        is_parent_pk = False
                elif 'actor' in response_dict['fields'] and ( str(response_dict['fields']['actor']) == '9' or str(response_dict['fields']['actor']) == '19' ):
                    is_parent_pk = True
            if is_parent_pk == True:
                with requests_cache.disabled():
                    if debug:
                        eprint(f"INFO: This is a parent UUID. Fetching trace=y for {response_id}")
                    response_content = requests.get(f"https://{ars_host}/ars/api/messages/" + response_id + '?trace=y', headers={'accept': 'application/json'})
                status_code = response_content.status_code

                if status_code != 200:
                    return( { "status": 404, "title": "Response not found", "detail": "Failed attempting to fetch trace=y from ARS with response_id="+str(response_id), "type": "about:blank" }, 404)

                #### Unpack the response content into a dict and dump
                try:
                    response_dict = response_content.json()
                except:
                    return( { "status": 404, "title": "Error decoding Response", "detail": "Cannot decode ARS response_id="+str(response_id)+" to a Translator Response", "type": "about:blank" }, 404)

                return response_dict

            if not is_parent_pk and 'fields' in response_dict and 'data' in response_dict['fields']:
                envelope = response_dict['fields']['data']
                if debug:
                    eprint(f"INFO: This is an ordinary child UUID. Reading and validating it...")
                if envelope is None:
                    envelope = {}
                    return envelope
                actual_response = str(envelope)
                if not isinstance(envelope,dict):
                    envelope = { 'detail': envelope }

                #### Actor lookup
                actor_lookup = { 
                    '1': 'Aragorn',
                    '2': 'ARAX',
                    '3': 'BTE',
                    '4': 'NCATS',
                    '5': 'Robokop',
                    '6': 'Unsecret',
                    '7': 'Genetics',
                    '8': 'MolePro',
                    '10': 'Explanatory',
                    '11': 'ImProving',
                    '12': 'Cam',
                    '13': 'TextMining'
                }

                #### Actor lookup by name
                actor_name_lookup = {
                    'ara-aragorn': 'Aragorn',
                    'ara-arax': 'ARAX',
                    'ara-bte': 'BTE',
                    'ara-ncats': 'NCATS',
                    'ara-robokop': 'Robokop',
                    'ara-unsecret': 'Unsecret',
                    'kp-genetics': 'Genetics',
                    'kp-molecular': 'MolePro',
                    'ara-explanatory': 'Explanatory',
                    'ara-improving': 'ImProving',
                    'kp-cam': 'Cam',
                    'kp-chp': 'CHP',
                    'kp-icees': 'ICEES',
                    'kp-openpredict': 'OpenPredict',
                    'kp-textmining': 'TextMining'
                }

                #Remove warning code hack
                #if 'logs' in envelope and envelope['logs'] is not None:
                #    for log in envelope['logs']:
                #        if isinstance(log,dict):
                #            if 'code' in log and log['code'] is None:
                #                log['code'] = '-'
                is_trapi = True
                if 'message' in envelope:
                    #eprint("INFO: envelope has a message")
                    #eprint(json.dumps(envelope,indent=2,sort_keys=True))
                    if 'logs' in envelope and isinstance(envelope['logs'],list) and len(envelope['logs']) > 0:
                        #eprint("INFO: envelope has logs")
                        if isinstance(envelope['logs'][0], str):
                           #eprint("INFO: logs[0] is str")
                           is_trapi = False
                           actual_response = envelope['logs'][0]
                           for i in range(len(envelope['logs'])):
                               if isinstance(envelope['logs'][i],str):
                                   envelope['logs'][i] = { 'level': 'INFO', 'message': 'ARS info: ' + envelope['logs'][i] }

                           try:
                               #eprint(f"INFO: Actual response: {actual_response}")
                               import html
                               actual_response = html.unescape(actual_response)
                               #eprint(f"INFO: Actual decoded response: {actual_response}")
                               actual_response_dict = json.loads(actual_response)
                               if 'message' in actual_response_dict:
                                   is_trapi = True
                                   envelope = actual_response_dict
                           except:
                               eprint("WARNING: tried to convert the response to JSON and it did not work")
                               eprint(f"It was: {envelope['logs'][0]}")

                else:
                    #eprint("INFO: envelope has no message")
                    is_trapi = False

                if not is_trapi:
                    envelope['validation_result'] = { 'status': 'NA', 'version': trapi_version, 'size': content_size, 'message': 'Returned response is not TRAPI: ' + actual_response }
                    return envelope


                #### Perform a validation on it
                try:
                    validate(envelope,'Response',trapi_version)
                    envelope['validation_result'] = { 'status': 'PASS', 'version': trapi_version, 'size': content_size, 'message': '' }

                except ValidationError as error:
                    timestamp = str(datetime.now().isoformat())
                    if 'logs' not in envelope or envelope['logs'] is None:
                        envelope['logs'] = []
                    envelope['logs'].append( { "code": 'InvalidTRAPI', "level": "ERROR", "message": "TRAPI validator reported an error: " + str(error),
                        "timestamp": timestamp } )
                    if 'description' not in envelope or envelope['description'] is None:
                        envelope['description'] = ''
                    envelope['validation_result'] = { 'status': 'FAIL', 'version': trapi_version, 'size': content_size, 'message': 'TRAPI validator reported an error: ' + str(error) + ' --- ' + envelope['description'] }

                #### Try to add the reasoner_id
                if 'name' in response_dict['fields'] and response_dict['fields']['name'] is not None:
                    #eprint(json.dumps(response_dict,indent=2,sort_keys=True))
                    actor = str(response_dict['fields']['name'])
                    if actor in actor_name_lookup:
                        #eprint(f"INFO: Attempting to inject {actor_name_lookup[actor]} into results")
                        if 'message' in envelope and 'results' in envelope['message'] and envelope['message']['results'] is not None:
                            for result in envelope['message']['results']:
                                if 'reasoner_id' in result and result['reasoner_id'] is not None:
                                    pass
                                else:
                                    result['reasoner_id'] = actor_name_lookup[actor]

                elif 'actor' in response_dict['fields'] and response_dict['fields']['actor'] is not None:
                    #eprint(json.dumps(response_dict,indent=2,sort_keys=True))
                    actor = str(response_dict['fields']['actor'])
                    if actor in actor_lookup:
                        if 'message' in envelope and 'results' in envelope['message'] and envelope['message']['results'] is not None:
                            for result in envelope['message']['results']:
                                if 'reasoner_id' in result and result['reasoner_id'] is not None:
                                    pass
                                else:
                                    result['reasoner_id'] = actor_lookup[actor]

                if 'message' in envelope and 'knowledge_graph' in envelope['message'] and envelope['message']['knowledge_graph'] is not None:
                    n_nodes = None
                    if 'nodes' in envelope['message']['knowledge_graph'] and envelope['message']['knowledge_graph']['nodes'] is not None:
                        n_nodes = len(envelope['message']['knowledge_graph']['nodes'])
                    n_edges = None
                    if 'edges' in envelope['message']['knowledge_graph'] and envelope['message']['knowledge_graph']['edges'] is not None:
                        n_edges = len(envelope['message']['knowledge_graph']['edges'])
                    envelope['validation_result']['n_nodes'] = n_nodes
                    envelope['validation_result']['n_edges'] = n_edges

                    #### Count provenance information
                    attribute_parser = ARAXAttributeParser(envelope,envelope['message'])
                    envelope['validation_result']['provenance_summary'] = attribute_parser.summarize_provenance_info()


                return envelope
            return( { "status": 404, "title": "Cannot find Response (in 'fields' and 'data') in ARS response packet", "detail": "Cannot decode ARS response_id="+str(response_id)+" to a Translator Response", "type": "about:blank" }, 404)


        return( { "status": 404, "title": "UnrecognizedResponse_idFormat", "detail": "Unrecognized response_id format", "type": "about:blank" }, 404)


    ##################################################################################################
    #### Store a received callback content
    def store_callback(self, body):

        data_dir = os.path.dirname(os.path.abspath(__file__)) + '/../../../data/callbacks'
        if not os.path.exists(data_dir):
            try:
                os.mkdir(data_dir)
            except:
                eprint(f"ERROR: Unable to create dir {data_dir}")
                return

        if os.path.exists(data_dir):
            counter = 1
            filename = f"{data_dir}/{counter:05}.json"
            while os.path.exists(filename):
                counter += 1
                filename = f"{data_dir}/{counter:05}.json"
                if counter > 5000:
                    eprint(f"ERROR: store_callback counter has reach 5000. Time to clean up or there is a runaway")
                    return

            try:
                with open(filename, 'w') as outfile:
                    json.dump(body, outfile, sort_keys=True, indent=2)
                    eprint(f"INFO: Received a response and wrote it to {filename}")
                    return
            except:
                eprint(f"ERROR: Unable to write response to file {filename}")
                return

        else:
            eprint(f"ERROR: Unable to find dir {data_dir}")











############################################ Main ############################################################

#### If this class is run from the command line, perform a short little test to see if it is working correctly
def main():

    #### Parse command line options
    import argparse
    argparser = argparse.ArgumentParser(description='CLI testing of the ResponseCache class')
    argparser.add_argument('--verbose', action='count', help='If set, print more information about ongoing processing' )
    #argparser.add_argument('list', action='store', help='List all local response ids')
    argparser.add_argument('response_id', type=str, nargs='*', help='Id of a response to fetch and display')
    params = argparser.parse_args()

    #### Create a new ResponseStore object
    response_cache = ResponseCache()

    #### Get the session handle
    session = response_cache.session

    #### Query and print some rows from the reference tables
    #if params.list is True:
    if False:
        print("Listing of all responses")
        for response in session.query(Response).all():
            print(f"response_id={response.response_id}  response_datetime={response.response_datetime}")
        return

    if len(params.response_id) > 0:
        print(f"Content of response_id {params.response_id[0]}:")
        envelope = response_cache.get_response(params.response_id[0])

        #print(json.dumps(ast.literal_eval(repr(envelope)), sort_keys=True, indent=2))
        #print(json.dumps(envelope, sort_keys=True, indent=2))
        print(json.dumps(envelope['logs'], sort_keys=True, indent=2))
        #return

    try:
        validate(envelope['message'],'Message',trapi_version)
        print('- Message is valid')
    except ValidationError as error:
        print(f"- Message INVALID: {error}")

    #return

    for component, klass in { 'query_graph': 'QueryGraph', 'knowledge_graph': 'KnowledgeGraph' }.items():
        if component in envelope['message']:
            try:
                validate(envelope['message'][component], klass, trapi_version)
                print(f"  - {component} is valid")
            except ValidationError:
                print(f"  - {component} INVALID")
        else:
            print(f"  - {component} is not present")

    for node_key, node in envelope['message']['knowledge_graph']['nodes'].items():
        print(f"{node_key}")
        for attribute in node['attributes']:
            attribute['value_type_id'] = None
            try:
                validate(attribute, 'Attribute', trapi_version)
                print(f"  - attribute with {attribute['attribute_type_id']} is valid")
            except ValidationError:
                print(f"  - attribute with {attribute['attribute_type_id']} is  INVALID")


    for result in envelope['message']['results']:
        try:
            validate(result,'Result', trapi_version)
            print(f"    - result is valid")
        except ValidationError:
            print(f"    - result INVALID")

        for key,node_binding_list in result['node_bindings'].items():
            for node_binding in node_binding_list:
                try:
                    validate(node_binding, 'NodeBinding', trapi_version)
                    print(f"      - node_binding {key} is valid")
                except ValidationError:
                    print(f"      - node_binding {key} INVALID")

        for key,edge_binding_list in result['edge_bindings'].items():
            for edge_binding in edge_binding_list:
                #print(json.dumps(edge_binding, sort_keys=True, indent=2))
                try:
                    validate(edge_binding,'EdgeBinding', trapi_version)
                    print(f"      - edge_binding {key} is valid")
                except ValidationError:
                    print(f"      - edge_binding {key} INVALID")





if __name__ == "__main__": main()


# code/ARAX/ResponseCache/test_response.py

#!/usr/bin/python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import sys
import re
import json
import requests
import json

from reasoner_validator import validate_Response, ValidationError



############################################ Main ############################################################

#### If this class is run from the command line, perform a short little test to see if it is working correctly
def main():

    #### Parse command line options
    import argparse
    argparser = argparse.ArgumentParser(description='CLI testing of the ResponseCache class')
    argparser.add_argument('--verbose', action='count', help='If set, print more information about ongoing processing' )
    argparser.add_argument('response_id', type=str, nargs='*', help='Integer number of a response to read and display')
    params = argparser.parse_args()

    #### Query and print some rows from the reference tables
    if len(params.response_id) == 0 or len(params.response_id) > 1:
        eprint("Please specify a single ARS response UUID")
        return

    response_id = params.response_id[0]

    response_content = requests.get('https://ars.transltr.io/ars/api/messages/'+response_id, headers={'accept': 'application/json'})
    status_code = response_content.status_code

    if status_code != 200:
        eprint("Cannot fetch from ARS a response corresponding to response_id="+str(response_id))
        return

    #### Unpack the response content into a dict
    try:
        response_dict = response_content.json()
    except:
        eprint("Cannot decode ARS response_id="+str(response_id)+" to a Translator Response")
        return

    if 'fields' in response_dict and 'actor' in response_dict['fields'] and str(response_dict['fields']['actor']) == '9':
            eprint("The supplied response id is a collection id. Please supply the UUID for a response")
            return

    if 'fields' in response_dict and 'data' in response_dict['fields']:
        envelope = response_dict['fields']['data']
        if envelope is None:
            envelope = {}
            return envelope

        #### Perform a validation on it
        try:
            validate_Response(envelope)
            eprint('Returned message is valid')

        except ValidationError as error:
            eprint('ERROR: TRAPI validator reported an error: ' + str(error))
            if params.verbose:
                print(json.dumps(envelope, sort_keys=True, indent=2))

    return


if __name__ == "__main__": main()


# code/ARAX/ResponseCache/upload_old_responses_to_s3.py

#!/usr/bin/python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast

import boto3
import timeit

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../")
from RTXConfiguration import RTXConfiguration


def main():

    rtx_config = RTXConfiguration()
    KEY_ID = rtx_config.config_secrets['s3']['access']
    ACCESS_KEY = rtx_config.config_secrets['s3']['secret']

    s3 = boto3.resource(
        's3',
        region_name='us-west-2',
        aws_access_key_id=KEY_ID,
        aws_secret_access_key=ACCESS_KEY
    )

    base_dir = '/mnt/data/orangeboard/Cache/responses_1_0'
    files = sorted(os.listdir(base_dir))
    #files = os.listdir(base_dir)

    test_mode = False
    counter = 0

    for file in files:

        with open(base_dir + '/' + file) as infile:

            content = ''
            for line in infile:
                content += line

            try:
                envelope = json.loads(content)
            except:
                print(f"ERROR: Unable to parse the JSON in {file}")
                continue

            response_filename = f"/responses/{file}"
            print(f"INFO: Writing {file} to bucket as {response_filename}")

            if not test_mode:
                s3.Object('arax-response-storage', response_filename).put(Body=content)

        counter += 1
        if counter > 999999:
            print("INFO: Ending early for testing")
            break


if __name__ == "__main__": main()


# code/ARAX/Testing/david_test.py

#!/usr/bin/python3
import sys

# This is basically just a query graph that I know will generate results that COHD can handle

def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)


import os
import json
import ast

sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../../UI/OpenAPI/python-flask-server")
from RTXQuery import RTXQuery


def main():
    #### Create an RTXQuery object
    rtxq = RTXQuery()

    #### Fill out a one hop query acetaminophen to proteins
    query = {
  "message": {
    "query_graph": {
      "edges": [
        {
          "id": "qg2",
          "negated": None,
          "relation": None,
          "source_id": "qg1",
          "target_id": "qg0",
          "type": "has_phenotype"
        }
      ],
      "nodes": [
        {
          "id": "qg1",
          "is_set": None,
          "name": "Atherosclerosis",
          "desc": "None",
          "curie": "DOID:1936",
          "type": "disease"
        },
        {
          "id": "qg0",
          "is_set": None,
          "name": None,
          "desc": "Generic phenotypic_feature",
          "curie": None,
          "type": "phenotypic_feature"
        }
      ]
    }
  },
  "max_results": 100
}

    #### Run the query and print the result
    message = rtxq.query(query)
    print(json.dumps(ast.literal_eval(repr(message.id)), sort_keys=True, indent=2))


if __name__ == "__main__": main()


# code/ARAX/Testing/test12_S3_put_get.py

#!/usr/bin/python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast

import boto3
import timeit

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../")
from RTXConfiguration import RTXConfiguration


def main():

    rtx_config = RTXConfiguration()
    KEY_ID = rtx_config.config_secrets['s3']['access']
    ACCESS_KEY = rtx_config.config_secrets['s3']['secret']

    query_graph = {
        "edges": {
            "e01": {
            "constraints": [],
            "object": "n1",
            "predicates": [
                "biolink:expression_decreased_by",
                "biolink:activity_decreased_by"
            ],
            "subject": "n0"
            }
        }
    }

    s3 = boto3.resource(
        's3',
        region_name='us-west-2',
        aws_access_key_id=KEY_ID,
        aws_secret_access_key=ACCESS_KEY
    )
    serialized_query_graph = json.dumps(query_graph,sort_keys=True,indent=2)
    print("INFO: Writing to bucket")
    t0 = timeit.default_timer()
    s3.Object('arax-response-storage', 'test1.txt').put(Body=serialized_query_graph)
    t1 = timeit.default_timer()
    print("Elapsed time: "+str(t1-t0))

    print("INFO: Reading from bucket")
    t0 = timeit.default_timer()
    
    content = s3.Object('arax-response-storage', 'test1.txt').get()["Body"].read()
    read_query_graph = json.loads(content)
    print(json.dumps(read_query_graph,sort_keys=True,indent=2))
    t1 = timeit.default_timer()
    print("Elapsed time: "+str(t1-t0))




if __name__ == "__main__": main()


# code/ARAX/Testing/test1_whatIsLovastatin.py

#!/usr/bin/python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../UI/OpenAPI/python-flask-server")
from RTXQuery import RTXQuery


def main():

    #### Create an RTXQuery object
    rtxq = RTXQuery()
 
    #### Fill out a simple what is query
    query = { "query_type_id": "Q0", "terms": { "term": "lovastatin" } }
    #query = { "query_type_id": "Q0", "terms": { "term": "lovastatin" }, "bypass_cache": "true" }  # Use bypass_cache if the cache if bad for this question

    #### Run the query and print the result
    message = rtxq.query(query)
    print(json.dumps(message.to_dict(),sort_keys=True,indent=2))


if __name__ == "__main__": main()


# code/ARAX/Testing/test2_acetaminophen2proteins.py

#!/usr/bin/python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../UI/OpenAPI/python-flask-server")
from RTXQuery import RTXQuery


def main():

    #### Create an RTXQuery object
    rtxq = RTXQuery()
 
    #### Fill out a one hop query acetaminophen to proteins
    query = {
        "message": {
            "query_graph": {
                "edges": [
                    {
                    "id": "qg2",
                    "source_id": "qg1",
                    "target_id": "qg0",
                    "type": "physically_interacts_with"
                    }
                ],
                "nodes": [
                    {
                    "id": "qg0",
                    "name": "acetaminophen",
                    "desc": "A member of the class of phenols that is 4-aminophenol in which one of the hydrogens attached to the amino group has been replaced by an acetyl group.",
                    "curie": "CHEMBL.COMPOUND:CHEMBL112",
                    "type": "chemical_substance"
                    },
                    {
                    "id": "qg1",
                    "name": None,
                    "desc": "Generic protein",
                    "curie": None,
                    "type": "protein"
                    }
                ]
            }
        }
    }

    #### Run the query and print the result
    message = rtxq.query(query)
    print(json.dumps(message.to_dict(),sort_keys=True,indent=2))


if __name__ == "__main__": main()


# code/ARAX/Testing/test3_inputgraph_processing.py

#!/usr/bin/python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
from ARAX_query import ARAXQuery

#### For debugging purposes, you can send all messages as they are logged to STDERR
from response import Response
Response.output = 'STDERR'


def main():

    #### Create an RTXQuery object
    araxq = ARAXQuery()
 
    #### Fill out a one hop query acetaminophen to proteins
    query = {
        "previous_message_processing_plan": {
            "previous_message_uris": [ "https://arax.ncats.io/api/rtx/v1/message/2" ],
            "processing_actions": [
                "filter(maximum_results=10)",
                "return(message=false,store=false)"
            ]
        }
    }

    #### Run the query and print the result
    message = araxq.query_return_message(query)
    print(json.dumps(message.to_dict(),sort_keys=True,indent=2))


if __name__ == "__main__": main()


# code/ARAX/Testing/test3_inputgraph_processing_via_API.py

#!/usr/bin/python3
""" This example sends a simple request to the ARAX API.
"""

import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import requests
import json
import ast


def main():

    #### Set the base URL for the reasoner and its endpoint
    #API_BASE_URL = 'https://arax.ncats.io/devED/api/rtx/v1'
    API_BASE_URL = 'http://localhost:5001/devED/api/rtx/v1'
    url_str = API_BASE_URL + "/query"
 
    #### Create a simple query to send to ARAX via the API
    query = {"previous_message_processing_plan": {"processing_actions": [
        "create_message",
        "add_qnode(name=DOID:731, id=n00, type=disease, is_set=false)",
        "add_qnode(type=phenotypic_feature, is_set=false, id=n01)",
        "add_qedge(source_id=n00, target_id=n01, id=e00)",
        "expand(edge_id=e00)",
        'resultify(ignore_edge_direction=true)',
        "return(message=true, store=false)"]}}

    #### Send the request to ARAX and check the status
    response_content = requests.post(url_str, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    if status_code == 302:
        print(response_content)
    elif status_code != 200:
        print("ERROR returned with status "+str(status_code))
        print(response_content.json())
        exit()

    #### Unpack the response content into a dict and dump
    response_dict = response_content.json()
    print(json.dumps(response_dict, indent=2, sort_keys=True))


if __name__ == "__main__": main()






# code/ARAX/Testing/test4_results_inference.py

#!/usr/bin/python3
import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
from ARAX_messenger import ARAXMessenger
from knowledge_graph_info import KnowledgeGraphInfo
from query_graph_info import QueryGraphInfo
from response import Response


def main():

    #### Create a response object
    response = Response()

    #### Read message #2 from the database. This should be the acetaminophen proteins query result message
    sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../UI/Feedback")
    from RTXFeedback import RTXFeedback
    araxdb = RTXFeedback()
    message_dict = araxdb.getMessage(2)

    #### The stored message comes back as a dict. Transform it to objects
    messenger = ARAXMessenger()
    message = messenger.from_dict(message_dict)
 
    #### Asses some information about the QueryGraph
    query_graph_info = QueryGraphInfo()
    result = query_graph_info.assess(message)
    response.merge(result)
    if result.status != 'OK':
        print(response.show(level=Response.DEBUG))
        return response
    #print(json.dumps(ast.literal_eval(repr(query_graph_info.node_order)),sort_keys=True,indent=2))

    #### Try to add query_graph_ids to the KnowledgeGraph
    knowledge_graph_info = KnowledgeGraphInfo()
    result = knowledge_graph_info.add_query_graph_tags(message,query_graph_info)
    response.merge(result)
    if result.status != 'OK':
        print(response.show(level=Response.DEBUG))
        return response

    #### Reassess some information about the KnowledgeGraph
    result = knowledge_graph_info.check_for_query_graph_tags(message,query_graph_info)
    response.merge(result)
    if result.status != 'OK':
        print(response.show(level=Response.DEBUG))
        return response

    #### Try to re-create the results list
    result = messenger.generate_results(message)
    response.merge(result)
    if result.status != 'OK':
        print(response.show(level=Response.DEBUG))
        return response


    print(response.show(level=Response.DEBUG))

    tmp = { 'query_graph_id_node_status': knowledge_graph_info.query_graph_id_node_status, 'query_graph_id_edge_status': knowledge_graph_info.query_graph_id_edge_status, 
        'n_nodes': knowledge_graph_info.n_nodes, 'n_edges': knowledge_graph_info.n_edges }
    #print(json.dumps(message.to_dict(),sort_keys=True,indent=2))
    print(json.dumps(ast.literal_eval(repr(tmp)),sort_keys=True,indent=2))




if __name__ == "__main__": main()


# code/ARAX/test/conftest.py

#!/usr/bin/env python3
import pytest
# Thanks https://docs.pytest.org/en/latest/example/simple.html#control-skipping-of-tests-according-to-command-line-option


def pytest_addoption(parser):
    parser.addoption(
        "--runslow", action="store_true", default=False, help="include slow tests"
    )
    parser.addoption(
        "--runonlyslow", action="store_true", default=False, help="run only slow tests"
    )
    parser.addoption(
        "--runexternal", action="store_true", default=False, help="include tests that rely on external KPs"
    )
    parser.addoption(
        "--runonlyexternal", action="store_true", default=False, help="run only external tests"
    )


def pytest_configure(config):
    config.addinivalue_line("markers", "slow: mark test as slow to run")
    config.addinivalue_line("markers", "external: mark test as relying on an external KP")


def pytest_collection_modifyitems(config, items):
    skip_slow = pytest.mark.skip(reason="need --runslow option to run")
    skip_fast = pytest.mark.skip(reason="--runonlyslow option was used; this test is fast")
    skip_external = pytest.mark.skip(reason="need --runexternal option to run")
    skip_internal = pytest.mark.skip(reason="--runonlyexternal option was used; this test is internal")
    for item in items:
        if "slow" in item.keywords:
            if not config.getoption("--runslow") and not config.getoption("--runonlyslow"):
                item.add_marker(skip_slow)
        elif config.getoption("--runonlyslow"):
            item.add_marker(skip_fast)

        if "external" in item.keywords:
            if not config.getoption("--runexternal") and not config.getoption("--runonlyexternal"):
                item.add_marker(skip_external)
        elif config.getoption("--runonlyexternal"):
            item.add_marker(skip_internal)


# code/ARAX/test/test_ARAX_connect.py

#!/usr/bin/env python3

# Intended to test ARAX connect

import sys
import os
import pytest
from collections import Counter
import copy
import json
import ast
from typing import List, Union

import numpy as np

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../ARAXQuery")
sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
from ARAX_query import ARAXQuery
from ARAX_response import ARAXResponse

PACKAGE_PARENT = '../../UI/OpenAPI/openapi_server'
sys.path.append(os.path.normpath(os.path.join(os.getcwd(), PACKAGE_PARENT)))
from openapi_server.models.message import Message


def _do_arax_query(query: dict) -> List[Union[ARAXResponse, Message]]:
    araxq = ARAXQuery()
    response = araxq.query(query)
    if response.status != 'OK':
        print(response.show(level=response.DEBUG))
    return [response, response.envelope.message]


def _attribute_tester(message, attribute_name: str, attribute_type: str, num_different_values=2):
    """
    Tests attributes of a message
    message: returned from _do_arax_query
    attribute_name: the attribute name to test (eg. 'jaccard_index')
    attribute_type: the attribute type (eg. 'EDAM:data_1234')
    num_different_values: the number of distinct values you wish to see have been added as attributes
    """
    edges_of_interest = []
    values = set()
    for key, edge in message.knowledge_graph.edges.items():
        if hasattr(edge, 'edge_attributes'):
            for attr in edge.edge_attributes:
                if attr.original_attribute_name == attribute_name:
                    edges_of_interest.append(edge)
                    assert attr.attribute_type_id == attribute_type
                    values.add(attr.value)
    assert len(edges_of_interest) > 0
    assert len(values) >= num_different_values


def _virtual_tester(message: Message, edge_predicate: str, relation: str, attribute_name: str, attribute_type: str, num_different_values=2):
    """
    Tests overlay functions that add virtual edges
    message: returned from _do_arax_query
    edge_predicate: the name of the virtual edge (eg. biolink:has_jaccard_index_with)
    relation: the relation you picked for the virtual_edge_relation (eg. N1)
    attribute_name: the attribute name to test (eg. 'jaccard_index')
    attribute_type: the attribute type (eg. 'EDAM:data_1234')
    num_different_values: the number of distinct values you wish to see have been added as attributes
    """
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert edge_predicate in edge_predicates_in_kg
    edges_of_interest = [x for x in message.knowledge_graph.edges.values() if x.relation == relation]
    values = set()
    assert len(edges_of_interest) > 0
    for edge in edges_of_interest:
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        assert edge.attributes[0].original_attribute_name == attribute_name
        values.add(edge.attributes[0].value)
        assert edge.attributes[0].attribute_type_id == attribute_type
    # make sure two or more values were added
    assert len(values) >= num_different_values


def test_connect_acetaminophen_to_ptgs1():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=CHEMBL.COMPOUND:CHEMBL112, key=n00)",
            "add_qnode(ids=UniProtKB:P23219, key=n01)",
            "connect(action=connect_nodes)",
            "resultify(ignore_edge_direction=true, debug=true)",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.query_graph.edges) > 0
    assert len(message.results) > 0


@pytest.mark.slow
def test_connect_nodes_1_hop():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:3312, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "connect(action=connect_nodes)",
            "resultify(ignore_edge_direction=true, debug=true)",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.query_graph.edges) > 0
    assert len(message.results) > 0


@pytest.mark.slow
def test_connect_nodes_2_hop():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=UniProtKB:P18509, key=n00)",
            "add_qnode(name=MESH:D004781, key=n01)",
            "connect(action=connect_nodes)",
            "resultify(ignore_edge_direction=true, debug=true)",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.query_graph.edges) == 2
    assert len(message.results) > 0


@pytest.mark.slow
def test_connect_nodes_subgraph_2_hop():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=UniProtKB:P54105,key=n02)",
            "add_qnode(name=UniProtKB:P18509,key=n00)",
            "add_qedge(subject=n00,object=n02,key=e00)",
            "expand(kp=infores:rtx-kg2,edge_key=e00)",
            "add_qnode(name=MESH:D004781,key=n01)",
            "connect(action=connect_nodes,qnode_keys=[n00,n01])",
            "resultify(ignore_edge_direction=true, debug=true)",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.query_graph.edges) == 3
    assert len(message.results) > 0

@pytest.mark.slow
def test_connect_nodes_3_disconnected_nodes():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=UniProtKB:P54105,key=n02)",
            "add_qnode(name=UniProtKB:P18509,key=n00)",
            "add_qnode(name=MESH:D004781,key=n01)",
            "connect(action=connect_nodes)",
            "resultify(ignore_edge_direction=true, debug=true)",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.query_graph.edges) >= 3
    assert len(message.results) > 0

@pytest.mark.slow
def test_1881():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=glucose, key=n0)",
            "add_qnode(name=diabetes, key=n1)",
            "connect(action=connect_nodes, max_path_length=3, shortest_path=false)",
            "resultify()",
            "filter_results(action=limit_number_of_results, max_results=30)",
            "return(message=true, store=true)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.query_graph.edges) >= 3
    assert len(message.results) > 0

@pytest.mark.slow
def test_none_object():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=glucose, key=n0)",
            "add_qnode(name=diabetes, key=n1)",
            "connect(action=connect_nodes, shortest_path=false)",
            "resultify()",
            "filter_results(action=limit_number_of_results, max_results=30)",
            "return(message=true, store=true)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.query_graph.edges) >= 1
    assert len(message.results) > 0


if __name__ == "__main__":
    pytest.main(['-v'])

# code/ARAX/test/test_ARAX_expand.py

#!/bin/env python3
"""
Usage:
    Run all expand tests: pytest -v test_ARAX_expand.py
    Run a single test: pytest -v test_ARAX_expand.py -k test_branched_query
"""

import sys
import os
from typing import List, Dict, Tuple, Optional

import pytest

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery/")
from ARAX_query import ARAXQuery
from ARAX_response import ARAXResponse
import Expand.expand_utilities as eu
sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../UI/OpenAPI/python-flask-server/")
from openapi_server.models.edge import Edge
from openapi_server.models.node import Node
from openapi_server.models.query_graph import QueryGraph
from openapi_server.models.attribute import Attribute
sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery/")
from biolink_helper import BiolinkHelper


def _run_query_and_do_standard_testing(actions: Optional[List[str]] = None, json_query: Optional[dict] = None,
                                       kg_should_be_incomplete=False, debug=False, should_throw_error=False,
                                       error_code: Optional[str] = None, timeout: Optional[int] = None) -> Tuple[Dict[str, Dict[str, Node]], Dict[str, Dict[str, Edge]]]:
    # Run the query
    araxq = ARAXQuery()
    assert actions or json_query  # Must provide some sort of query to run
    query_object = {"operations": {"actions": actions}} if actions else {"message": {"query_graph": json_query}}
    if timeout:
        query_object["query_options"] = {"kp_timeout": timeout}
    response = araxq.query(query_object)
    message = araxq.message
    if response.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
    assert response.status == 'OK' or should_throw_error
    if should_throw_error and error_code:
        assert response.error_code == error_code

    # Convert output knowledge graph to a dictionary format for faster processing (organized by QG IDs)
    dict_kg = eu.convert_standard_kg_to_qg_organized_kg(message.knowledge_graph)
    nodes_by_qg_id = dict_kg.nodes_by_qg_id
    edges_by_qg_id = dict_kg.edges_by_qg_id

    # Optionally print more detail
    if debug:
        print_nodes(nodes_by_qg_id)
        print_edges(edges_by_qg_id)
        print_counts_by_qgid(nodes_by_qg_id, edges_by_qg_id)
        print(response.show(level=ARAXResponse.DEBUG))

    # Run standard testing (applies to every test case)
    assert eu.qg_is_fulfilled(message.query_graph, dict_kg, enforce_required_only=True) or kg_should_be_incomplete or should_throw_error
    check_for_orphans(nodes_by_qg_id, edges_by_qg_id)
    check_property_format(nodes_by_qg_id, edges_by_qg_id)
    check_node_categories(message.knowledge_graph.nodes, message.query_graph)

    return nodes_by_qg_id, edges_by_qg_id


def print_counts_by_qgid(nodes_by_qg_id: Dict[str, Dict[str, Node]], edges_by_qg_id: Dict[str, Dict[str, Edge]]):
    print(f"KG counts:")
    if nodes_by_qg_id or edges_by_qg_id:
        for qnode_key, corresponding_nodes in sorted(nodes_by_qg_id.items()):
            print(f"  {qnode_key}: {len(corresponding_nodes)}")
        for qedge_key, corresponding_edges in sorted(edges_by_qg_id.items()):
            print(f"  {qedge_key}: {len(corresponding_edges)}")
    else:
        print("  KG is empty")


def print_nodes(nodes_by_qg_id: Dict[str, Dict[str, Node]]):
    for qnode_key, nodes in sorted(nodes_by_qg_id.items()):
        for node_key, node in sorted(nodes.items()):
            print(f"{qnode_key}: {node.categories}, {node_key}, {node.name}, {node.qnode_keys}, "
                  f"{node.query_ids if hasattr(node, 'query_ids') else ''}")


def print_edges(edges_by_qg_id: Dict[str, Dict[str, Edge]]):
    for qedge_key, edges in sorted(edges_by_qg_id.items()):
        for edge_key, edge in sorted(edges.items()):
            print(f"{qedge_key}: {edge_key}, {edge.subject}--{edge.predicate}->{edge.object}, {edge.qedge_keys}")


def check_for_orphans(nodes_by_qg_id: Dict[str, Dict[str, Node]], edges_by_qg_id: Dict[str, Dict[str, Edge]]):
    node_keys = set()
    node_keys_used_by_edges = set()
    for qnode_key, nodes in nodes_by_qg_id.items():
        for node_key, node in nodes.items():
            node_keys.add(node_key)
    for qedge_key, edges in edges_by_qg_id.items():
        for edge_key, edge in edges.items():
            node_keys_used_by_edges.add(edge.subject)
            node_keys_used_by_edges.add(edge.object)
    assert node_keys == node_keys_used_by_edges or len(node_keys_used_by_edges) == 0


def check_property_format(nodes_by_qg_id: Dict[str, Dict[str, Node]], edges_by_qg_id: Dict[str, Dict[str, Edge]]):
    for qnode_key, nodes in nodes_by_qg_id.items():
        for node_key, node in nodes.items():
            assert node_key and isinstance(node_key, str)
            assert node.qnode_keys and isinstance(node.qnode_keys, list)
            assert isinstance(node.name, str) or node.name is None
            assert isinstance(node.categories, list) or node.categories is None
            if node.attributes:
                for attribute in node.attributes:
                    _check_attribute(attribute)
    for qedge_key, edges in edges_by_qg_id.items():
        for edge_key, edge in edges.items():
            assert edge_key and isinstance(edge_key, str)
            assert edge.qedge_keys and isinstance(edge.qedge_keys, list)
            assert edge.subject and isinstance(edge.subject, str)
            assert edge.object and isinstance(edge.object, str)
            assert isinstance(edge.predicate, str) or edge.predicate is None
            if edge.attributes:
                for attribute in edge.attributes:
                    _check_attribute(attribute)


def _check_attribute(attribute: Attribute):
    assert attribute.attribute_type_id and isinstance(attribute.attribute_type_id, str)
    assert attribute.value is not None and (isinstance(attribute.value, str) or isinstance(attribute.value, list) or
                                            isinstance(attribute.value, int) or isinstance(attribute.value, float) or
                                            isinstance(attribute.value, dict))
    assert isinstance(attribute.value_type_id, str) or attribute.value_type_id is None
    assert isinstance(attribute.value_url, str) or attribute.value_url is None
    assert isinstance(attribute.attribute_source, str) or attribute.attribute_source is None
    assert isinstance(attribute.original_attribute_name, str) or attribute.original_attribute_name is None
    assert isinstance(attribute.description, str) or attribute.description is None


def check_node_categories(nodes: Dict[str, Node], query_graph: QueryGraph):
    bh = BiolinkHelper()
    qnode_descendant_categories_map = {qnode_key: set(bh.get_descendants(qnode.categories))
                                       for qnode_key, qnode in query_graph.nodes.items() if qnode.categories}
    for node in nodes.values():
        for qnode_key in node.qnode_keys:
            qnode = query_graph.nodes[qnode_key]
            if qnode.categories and not qnode.ids:
                # A node's categories should be only descendants of what was asked for in the QG
                assert set(node.categories).issubset(qnode_descendant_categories_map[qnode_key])


@pytest.mark.slow
def test_720_multiple_qg_ids_in_different_results():
    actions_list = [
        "add_qnode(key=n00, ids=MONDO:0014324)",
        "add_qnode(key=n01, categories=biolink:Protein)",
        "add_qnode(key=n02, categories=biolink:ChemicalEntity)",
        "add_qnode(key=n03, categories=biolink:Protein)",
        "add_qedge(key=e00, subject=n00, object=n01)",
        "add_qedge(key=e01, subject=n01, object=n02, predicates=biolink:physically_interacts_with)",
        "add_qedge(key=e02, subject=n02, object=n03, predicates=biolink:physically_interacts_with)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)
    assert set(nodes_by_qg_id['n01']).intersection(set(nodes_by_qg_id['n03']))
    assert any(set(node.qnode_keys) == {'n01', 'n03'} for node in nodes_by_qg_id['n01'].values())


@pytest.mark.external
def test_bte_query():
    actions_list = [
        "add_qnode(ids=UniProtKB:P16471, categories=biolink:Protein, key=n00)",
        "add_qnode(categories=biolink:Cell, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(kp=infores:biothings-explorer)",
        "return(message=true, store=false)",
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


def test_single_node_query_with_synonyms():
    actions_list = [
        "add_qnode(key=n00, ids=CHEMBL.COMPOUND:CHEMBL1771)",
        "expand(node_key=n00, kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


def test_single_node_query_with_no_results():
    actions_list = [
        "add_qnode(key=n00, ids=FAKE:curie)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list, kg_should_be_incomplete=True)
    assert not nodes_by_qg_id and not edges_by_qg_id


def test_single_node_query_with_list():
    actions_list = [
        "add_qnode(key=n00, ids=[CHEMBL.COMPOUND:CHEMBL108, CHEMBL.COMPOUND:CHEMBL110])",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)
    assert len(nodes_by_qg_id['n00']) == 2


@pytest.mark.slow
def test_branched_query():
    actions_list = [
        "add_qnode(key=n00, ids=DOID:0060227)",  # Adams-Oliver
        "add_qnode(key=n01, categories=biolink:PhenotypicFeature, is_set=true)",
        "add_qnode(key=n02, categories=biolink:Disease)",
        "add_qnode(key=n03, categories=biolink:Protein, is_set=true)",
        "add_qedge(subject=n01, object=n00, key=e00)",
        "add_qedge(subject=n02, object=n00, key=e01)",
        "add_qedge(subject=n00, object=n03, key=e02)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.slow
def test_query_that_expands_same_edge_twice():
    actions_list = [
        "add_qnode(key=n00, ids=DOID:9065, categories=biolink:Disease)",
        "add_qnode(key=n01, categories=biolink:ChemicalEntity)",
        "add_qedge(key=e00, subject=n00, object=n01, predicates=biolink:treats)",
        "expand(kp=infores:rtx-kg2)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


def test_771_continue_if_no_results_query():
    actions_list = [
        "add_qnode(ids=UniProtKB:P14136, key=n00)",
        "add_qnode(ids=NOTAREALCURIE, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list, kg_should_be_incomplete=True)
    assert 'n01' not in nodes_by_qg_id
    assert 'e00' not in edges_by_qg_id


@pytest.mark.slow
def test_774_continue_if_no_results_query():
    actions_list = [
        "add_qnode(ids=CHEMBL.COMPOUND:CHEMBL112, key=n1)",
        "add_qnode(ids=DOID:8295, key=n2)",
        "add_qedge(subject=n1, object=n2, key=e1)",
        "expand(edge_key=e1, kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list, kg_should_be_incomplete=True)
    assert not nodes_by_qg_id and not edges_by_qg_id


def test_curie_list_query():
    actions_list = [
        "add_qnode(ids=[DOID:6419, DOID:3717, DOID:11406], key=n00)",
        "add_qnode(categories=biolink:PhenotypicFeature, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)
    assert len(nodes_by_qg_id["n00"]) >= 3


@pytest.mark.slow
def test_query_with_curies_on_both_ends():
    actions_list = [
        "add_qnode(ids=MONDO:0005393, key=n00)",  # Gout
        "add_qnode(ids=UMLS:C0018100, key=n01)",  # Antigout agents
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.slow
def test_query_with_intermediate_curie_node():
    actions_list = [
        "add_qnode(categories=biolink:Protein, key=n00, is_set=True)",
        "add_qnode(ids=HP:0005110, key=n01)",  # atrial fibrillation
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:treats)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:related_to)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


def test_847_dont_expand_curie_less_edge():
    actions_list = [
        "add_qnode(key=n00, categories=biolink:Protein)",
        "add_qnode(key=n01, categories=biolink:ChemicalEntity)",
        "add_qedge(key=e00, subject=n00, object=n01)",
        "expand(edge_key=e00, kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list, should_throw_error=True,
                                                                        error_code="QueryGraphNoIds")


@pytest.mark.slow
def test_deduplication_and_self_edges():
    actions_list = [
        "add_qnode(ids=UMLS:C0004572, key=n00)",  # Babesia
        "add_qnode(key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(edge_key=e00, kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)
    # Check that deduplication worked appropriately
    all_node_keys = {node_key for nodes in nodes_by_qg_id.values() for node_key in nodes}
    babesia_curies = {"UMLS:C0004572", "CHV:0000001647", "LNC:LP19999-9", "MEDDRA:10003963", "MESH:D001403",
                      "NCIT:C122040", "NCI_CDISC:C122040", "SNOMEDCT:35029001"}
    babesia_curies_in_answer = all_node_keys.intersection(babesia_curies)
    assert len(babesia_curies_in_answer) <= 1
    # Check that we don't have any self-edges
    self_edges = [edge for edge in edges_by_qg_id['e00'].values() if edge.subject == edge.object]
    assert not self_edges


@pytest.mark.slow
def test_873_consider_both_gene_and_protein():
    actions_list_protein = [
        "add_qnode(ids=DOID:9452, key=n00)",
        "add_qnode(categories=biolink:Protein, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)",
    ]
    nodes_by_qg_id_protein, edges_by_qg_id_protein = _run_query_and_do_standard_testing(actions_list_protein)
    actions_list_gene = [
        "add_qnode(ids=DOID:9452, key=n00)",
        "add_qnode(categories=biolink:Gene, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)",
    ]
    nodes_by_qg_id_gene, edges_by_qg_id_gene = _run_query_and_do_standard_testing(actions_list_gene)
    assert set(nodes_by_qg_id_protein['n01']) == set(nodes_by_qg_id_gene['n01'])


def test_987_override_node_categories():
    actions_list = [
        "add_qnode(name=DOID:8398, key=n00)",
        "add_qnode(categories=biolink:PhenotypicFeature, key=n01)",
        "add_qedge(subject=n00, object=n01, predicates=biolink:has_phenotype, key=e00)",
        "expand(edge_key=e00, kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)
    assert all('biolink:PhenotypicFeature' in node.categories for node in nodes_by_qg_id['n01'].values())


@pytest.mark.external
def test_cohd_expand():
    actions_list = [
        "add_qnode(ids=MONDO:0005301, key=n00)",
        "add_qnode(categories=biolink:SmallMolecule, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:correlated_with)",
        "expand(edge_key=e00, kp=infores:cohd)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


def test_dtd_expand_1():
    actions_list = [
        "add_qnode(name=acetaminophen, key=n0)",
        "add_qnode(name=Sotos syndrome, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:arax-drug-treats-disease, DTD_threshold=0, DTD_slow_mode=True)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)
    assert all([edges_by_qg_id[qedge_key][edge_key].predicate == "biolink:probability_treats" for qedge_key in edges_by_qg_id for edge_key in edges_by_qg_id[qedge_key]])
    assert all([edges_by_qg_id[qedge_key][edge_key].attributes[0].original_attribute_name == "probability_treats" for qedge_key in edges_by_qg_id for edge_key in edges_by_qg_id[qedge_key]])
    assert all([edges_by_qg_id[qedge_key][edge_key].attributes[0].attribute_type_id == "EDAM:data_0951" for qedge_key in edges_by_qg_id for edge_key in edges_by_qg_id[qedge_key]])
    assert all([edges_by_qg_id[qedge_key][edge_key].attributes[0].value_url == "https://doi.org/10.1101/765305" for qedge_key in edges_by_qg_id for edge_key in edges_by_qg_id[qedge_key]])


@pytest.mark.slow
def test_dtd_expand_2():
    actions_list = [
        "add_qnode(name=acetaminophen, key=n0)",
        "add_qnode(categories=biolink:Disease, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:arax-drug-treats-disease, DTD_threshold=0, DTD_slow_mode=True)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)
    assert all([edges_by_qg_id[qedge_key][edge_key].predicate == "biolink:probability_treats" for qedge_key in edges_by_qg_id for edge_key in edges_by_qg_id[qedge_key]])
    assert all([edges_by_qg_id[qedge_key][edge_key].attributes[0].original_attribute_name == "probability_treats" for qedge_key in edges_by_qg_id for edge_key in edges_by_qg_id[qedge_key]])
    assert all([edges_by_qg_id[qedge_key][edge_key].attributes[0].attribute_type_id == "EDAM:data_0951" for qedge_key in edges_by_qg_id for edge_key in edges_by_qg_id[qedge_key]])
    assert all([edges_by_qg_id[qedge_key][edge_key].attributes[0].value_url == "https://doi.org/10.1101/765305" for qedge_key in edges_by_qg_id for edge_key in edges_by_qg_id[qedge_key]])


def test_ngd_expand():
    actions_list = [
        "add_qnode(name=MONDO:0007156, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(kp=infores:arax-normalized-google-distance)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.external
def test_chp_expand_1():
    actions_list = [
        "add_qnode(ids=ENSEMBL:ENSG00000162419, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(edge_key=e00, kp=infores:connections-hypothesis)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.external
def test_chp_expand_2():
    actions_list = [
        "add_qnode(ids=[ENSEMBL:ENSG00000124532,ENSEMBL:ENSG00000075975,ENSEMBL:ENSG00000104774], key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(edge_key=e00, kp=infores:connections-hypothesis)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.external
def test_genetics_kp():
    actions_list = [
        "add_qnode(ids=NCBIGene:1803, categories=biolink:Gene, key=n00)",
        "add_qnode(categories=biolink:Disease, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:gene_associated_with_condition)",
        "expand(kp=infores:genetics-data-provider)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.external
def test_molepro_query():
    actions_list = [
        "add_qnode(ids=HGNC:9379, categories=biolink:Gene, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(kp=infores:molepro)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.external
def test_spoke_query():
    actions_list = [
        "add_qnode(ids=NCBIGene:7157, categories=biolink:Gene, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n01, object=n00, key=e00, predicates=biolink:molecularly_interacts_with)",
        "expand(kp=infores:spoke)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.external
def test_spoke_query_2():
    actions_list = [
        "add_qnode(ids=NCBIGene:7157, categories=biolink:Gene, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:molecularly_interacts_with)",
        "expand(kp=infores:spoke)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.slow
def test_exclude_edge_parallel():
    # First run a query without any kryptonite edges to get a baseline
    actions_list = [
        "add_qnode(name=DOID:3312, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n01, object=n00, predicates=biolink:treats, key=e00)",
        "add_qedge(subject=n01, object=n00, predicates=biolink:causes, key=e01)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)
    contraindicated_pairs = {tuple(sorted([edge.subject, edge.object])) for edge in edges_by_qg_id["e00"].values()}
    assert contraindicated_pairs

    # Then exclude the contraindicated edge and make sure the appropriate nodes are blown away
    actions_list = [
        "add_qnode(name=DOID:3312, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n01, object=n00, predicates=biolink:treats, key=e00)",
        "add_qedge(subject=n01, object=n00, predicates=biolink:causes, exclude=true, key=e01)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id_not, edges_by_qg_id_not = _run_query_and_do_standard_testing(actions_list)
    # None of the contraindicated n01 nodes should appear in the answer this time
    final_pairs = {tuple(sorted([edge.subject, edge.object])) for edge in edges_by_qg_id_not["e00"].values()}
    assert not contraindicated_pairs.intersection(final_pairs)
    assert "e01" not in edges_by_qg_id_not


@pytest.mark.slow
def test_exclude_edge_perpendicular():
    exclude_curies = ", ".join(['CHEMBL.COMPOUND:CHEMBL190', 'CHEMBL.COMPOUND:CHEMBL775'])
    # First run a query without any kryptonite edges to get a baseline
    actions_list = [
        "add_qnode(ids=DOID:3312, key=n00)",
        "add_qnode(categories=biolink:Protein, key=n01, is_set=true)",
        f"add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n01, object=n00, key=e00, predicates=biolink:causes)",
        "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:entity_positively_regulates_entity)",
        # 'Exclude' portion (just optional for now to get a baseline)
        f"add_qnode(categories=biolink:ChemicalEntity, key=nx0, option_group_id=1, ids=[{exclude_curies}])",
        "add_qedge(subject=n01, object=nx0, key=ex0, option_group_id=1, predicates=biolink:entity_negatively_regulates_entity)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)
    nodes_used_by_kryptonite_edge = eu.get_node_keys_used_by_edges(edges_by_qg_id["ex0"])
    n01_nodes_to_blow_away = set(nodes_by_qg_id["n01"]).intersection(nodes_used_by_kryptonite_edge)
    assert n01_nodes_to_blow_away

    # Then use a kryptonite edge and make sure the appropriate nodes are blown away
    actions_list = [
        "add_qnode(ids=DOID:3312, key=n00)",
        "add_qnode(categories=biolink:Protein, key=n01, is_set=true)",
        f"add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n01, object=n00, key=e00, predicates=biolink:causes)",
        "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:entity_positively_regulates_entity)",
        # 'Exclude' portion
        f"add_qnode(categories=biolink:ChemicalEntity, key=nx0, ids=[{exclude_curies}])",
        "add_qedge(subject=n01, object=nx0, key=ex0, exclude=True, predicates=biolink:entity_negatively_regulates_entity)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id_not, edges_by_qg_id_not = _run_query_and_do_standard_testing(actions_list)
    assert not n01_nodes_to_blow_away.intersection(set(nodes_by_qg_id_not["n01"]))
    assert "ex0" not in edges_by_qg_id_not and "nx0" not in nodes_by_qg_id_not


@pytest.mark.slow
def test_exclude_edge_ordering():
    # This test makes sures that kryptonite qedges are expanded AFTER their adjacent qedges
    actions_list = [
        "add_qnode(name=DOID:3312, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n00, object=n01, predicates=biolink:treats, key=e00)",
        "add_qedge(subject=n00, object=n01, predicates=biolink:predisposes, exclude=true, key=e01)",
        "expand(kp=infores:rtx-kg2, edge_key=e00)",
        "expand(kp=infores:rtx-kg2, edge_key=e01)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id_a, edges_by_qg_id_a = _run_query_and_do_standard_testing(actions_list)
    actions_list = [
        "add_qnode(name=DOID:3312, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n00, object=n01, predicates=biolink:treats, key=e00)",
        "add_qedge(subject=n00, object=n01, predicates=biolink:predisposes, exclude=true, key=e01)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id_b, edges_by_qg_id_b = _run_query_and_do_standard_testing(actions_list)
    actions_list = [
        "add_qnode(name=DOID:3312, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n00, object=n01, predicates=biolink:predisposes, exclude=true, key=e01)",
        "add_qedge(subject=n00, object=n01, predicates=biolink:treats, key=e00)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id_c, edges_by_qg_id_c = _run_query_and_do_standard_testing(actions_list)
    # All of these queries should produce the same KG contents
    assert set(nodes_by_qg_id_a["n01"]) == set(nodes_by_qg_id_b["n01"]) == set(nodes_by_qg_id_c["n01"])
    assert set(edges_by_qg_id_a["e00"]) == set(edges_by_qg_id_b["e00"]) == set(edges_by_qg_id_c["e00"])


def test_exclude_edge_no_results():
    # Tests query with an exclude edge that doesn't have any matches in the KP (shouldn't error out)
    actions = [
        "add_qnode(name=DOID:3312, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n00, object=n01, predicates=biolink:treats, key=e00)",
        "add_qedge(subject=n00, object=n01, predicates=biolink:not_a_real_edge_type, exclude=true, key=e01)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions)


def test_option_group_query_one_hop():
    # Tests a simple one-hop query with an optional edge
    actions = [
        "add_qnode(key=n00, ids=DOID:3312)",
        "add_qnode(key=n01, categories=biolink:ChemicalEntity)",
        "add_qedge(key=e00, subject=n01, object=n00, predicates=biolink:causes)",
        "add_qedge(key=e01, subject=n00, object=n01, predicates=biolink:affects, option_group_id=1)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions)


@pytest.mark.slow
def test_option_group_query_no_results():
    # Tests query with optional path that doesn't have any matches in the KP (shouldn't error out)
    actions = [
        "add_qnode(key=n00, ids=DOID:3312)",
        "add_qnode(key=n01, ids=CHEBI:48607)",
        "add_qnode(key=n02, categories=biolink:Protein, option_group_id=1, is_set=true)",
        "add_qedge(key=e00, subject=n00, object=n01, predicates=biolink:related_to)",
        "add_qedge(key=e01, subject=n00, object=n02, option_group_id=1, predicates=biolink:overlaps)",
        "add_qedge(key=e02, subject=n02, object=n01, option_group_id=1, predicates=biolink:affects)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions)


def test_category_and_predicate_format():
    actions_list = [
        "add_qnode(ids=UniProtKB:P42857, key=n00)",
        "add_qnode(categories=biolink:Protein, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:affects)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)
    for qnode_key, nodes in nodes_by_qg_id.items():
        for node_key, node in nodes.items():
            assert all(category.startswith("biolink:") for category in node.categories)
    for qedge_key, edges in edges_by_qg_id.items():
        for edge_key, edge in edges.items():
            assert edge.predicate.startswith("biolink:")
            assert "," not in edge.predicate


def test_issue_1212():
    # If a qnode curie isn't recognized by synonymizer, shouldn't end up with results when using KG2c
    actions_list = [
        "add_qnode(ids=FAKE:Curie, categories=biolink:ChemicalEntity, key=n00)",
        "add_qnode(categories=biolink:Disease, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list, kg_should_be_incomplete=True)


def test_issue_1314():
    # KG2 should return answers for "treated_by" (even though it only contains "treats" edges)
    actions_list = [
        "add_qnode(key=n0, ids=DRUGBANK:DB00394, categories=biolink:ChemicalEntity)",
        "add_qnode(key=n1, categories=biolink:Disease)",
        "add_qedge(key=e0, subject=n1, object=n0, predicates=biolink:treated_by)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.external
def test_issue_1236_a():
    # Test that multiple KPs are used for expansion when no KP is specified in DSL
    actions_list = [
        "add_qnode(ids=NCBIGene:1803, key=n00)",
        "add_qnode(categories=biolink:Disease, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:gene_associated_with_condition)",
        "expand()",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)

    actions_list_kg2_only = [
        "add_qnode(ids=NCBIGene:1803, key=n00)",
        "add_qnode(categories=biolink:Disease, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:gene_associated_with_condition)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id_kg2_only, edges_by_qg_id_kg2_only = _run_query_and_do_standard_testing(actions_list_kg2_only)

    assert len(nodes_by_qg_id["n01"]) > len(nodes_by_qg_id_kg2_only["n01"])


def test_issue_1236_b():
    actions_list = [
        "add_qnode(ids=DOID:14330, categories=biolink:Disease, key=n00)",
        "add_qnode(categories=biolink:Protein, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:condition_associated_with_gene)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


def test_kg2_predicate_hierarchy_reasoning():
    actions_list = [
        "add_qnode(ids=CHEMBL.COMPOUND:CHEMBL112, categories=biolink:ChemicalEntity, key=n00)",
        "add_qnode(categories=biolink:Protein, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:affects)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)
    assert any(edge for edge in edges_by_qg_id["e00"].values() if edge.predicate == "biolink:affects")
    assert any(edge for edge in edges_by_qg_id["e00"].values() if edge.predicate == "biolink:entity_positively_regulates_entity")
    assert not any(edge for edge in edges_by_qg_id["e00"].values() if edge.predicate == "biolink:related_to")


@pytest.mark.slow
def test_issue_1373_pinned_curies():
    actions_list = [
        "add_qnode(ids=chembl.compound:CHEMBL2108129, key=n00)",
        "add_qnode(categories=biolink:Protein, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:physically_interacts_with)",
        "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:physically_interacts_with)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)
    assert "CHEMBL.COMPOUND:CHEMBL2108129" not in nodes_by_qg_id["n02"]


@pytest.mark.external
def test_multiomics_clinical_risk_kp():
    actions_list = [
        "add_qnode(ids=DOID:14330, categories=biolink:Disease, key=n00)",
        "add_qnode(categories=biolink:PhenotypicFeature, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:related_to)",
        "expand(kp=infores:biothings-multiomics-clinical-risk)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.external
def test_multiomics_wellness_kp():
    actions_list = [
        "add_qnode(ids=UniProtKB:O00533, categories=biolink:Protein, key=n00)",
        "add_qnode(categories=biolink:Protein, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(kp=infores:biothings-multiomics-wellness)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.external
def test_multiomics_drug_response_kp():
    actions_list = [
        "add_qnode(ids=NCBIGene:7157, categories=biolink:Gene, key=n00)",
        "add_qnode(categories=biolink:SmallMolecule, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:associated_with_sensitivity_to)",
        "expand(kp=infores:biothings-multiomics-biggim-drug-response)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.external
def test_multiomics_tumor_gene_mutation_kp():
    actions_list = [
        "add_qnode(ids=MONDO:0018177, key=n00)",
        "add_qnode(categories=biolink:Gene, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(kp=infores:biothings-tcga-mut-freq)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


def test_many_kp_query():
    actions_list = [
        "add_qnode(ids=CHEMBL.COMPOUND:CHEMBL112, key=n00)",
        "add_qnode(categories=biolink:Protein, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:interacts_with)",
        "expand()",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list, timeout=10)


def test_entity_to_entity_query():
    actions_list = [
        "add_qnode(ids=NCBIGene:375, categories=biolink:Gene, key=n0)",
        "add_qnode(categories=biolink:Gene, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0, predicates=biolink:entity_negatively_regulates_entity)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


def test_1516_single_quotes_in_ids():
    actions = [
        "add_qnode(key=n0,ids=UniProtKB:P00491)",
        "add_qnode(key=n1)",
        "add_qedge(key=e01,subject=n0,object=n1)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions)


def test_input_curie_remapping():
    actions = [
        "add_qnode(key=n0, ids=KEGG.COMPOUND:C02700)",
        "add_qnode(key=n1, categories=biolink:Protein)",
        "add_qedge(key=e01, subject=n0, object=n1)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions)
    assert "KEGG.COMPOUND:C02700" in nodes_by_qg_id["n0"]
    assert "formylkynurenine" in nodes_by_qg_id["n0"]["KEGG.COMPOUND:C02700"].name.lower()


def test_constraint_validation():
    query = {
      "edges": {
        "e00": {
          "object": "n01",
          "predicates": ["biolink:physically_interacts_with"],
          "subject": "n00",
          "attribute_constraints": [{"id": "test_edge_constraint_1", "name": "test name edge", "operator": "<", "value": 1.0},
                                    {"id": "test_edge_constraint_2", "name": "test name edge", "operator": ">", "value": 0.5}]
        }
      },
      "nodes": {
        "n00": {
          "categories": ["biolink:ChemicalEntity"],
          "ids": ["CHEMBL.COMPOUND:CHEMBL112"]
        },
        "n01": {
          "categories": ["biolink:Protein"],
          "constraints": [{"id": "test_node_constraint", "name": "test name node", "operator": "<", "value": 1.0}]
        }
      }
    }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query, should_throw_error=True,
                                                                        error_code="UnsupportedConstraint")


def test_edge_constraints():
    query = {
            "nodes": {
                "n00": {
                    "ids": ["CHEMBL.COMPOUND:CHEMBL112"]
                },
                "n01": {
                    "categories": ["biolink:ChemicalEntity"]
                }
            },
            "edges": {
                "e00": {
                    "object": "n00",
                    "subject": "n01",
                    "attribute_constraints": [
                        {
                            "id": "biolink:knowledge_source",
                            "name": "knowledge source",
                            "value": ["infores:rtx-kg2","infores:arax","infores:drugbank"],
                            "operator": "==",
                            "not": False
                        }
                    ]
                }
            }
        }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query)


def test_canonical_predicates():
    actions = [
        "add_qnode(key=n00, ids=CHEMBL.COMPOUND:CHEMBL945)",
        "add_qnode(key=n01, categories=biolink:BiologicalEntity)",
        "add_qedge(key=e00, subject=n00, object=n01, predicates=biolink:participates_in)",  # Not canonical
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions)
    e00_predicates = {edge.predicate for edge in edges_by_qg_id["e00"].values()}
    assert "biolink:has_participant" in e00_predicates and "biolink:participates_in" not in e00_predicates


@pytest.mark.slow
@pytest.mark.external
def test_curie_prefix_conversion_1537():
    actions = [
        "add_qnode(key=n0, ids=NCBIGene:60412, categories=biolink:Gene)",
        "add_qnode(key=n1, categories=biolink:ChemicalEntity)",
        "add_qedge(key=e01, subject=n0, object=n1, predicates=biolink:related_to)",
        "expand(kp=infores:connections-hypothesis)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions)


@pytest.mark.slow
@pytest.mark.external
def test_merging_node_attributes_1450():
    actions = [
        "add_qnode(key=n0, ids=CHEMBL.COMPOUND:CHEMBL112)",
        "add_qnode(key=n1, categories=biolink:Disease)",
        "add_qedge(key=e01, subject=n0, object=n1, predicates=biolink:treats)",
        "expand(kp=infores:biothings-explorer)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions)
    num_attributes_a = len(nodes_by_qg_id["n0"]["CHEMBL.COMPOUND:CHEMBL112"].attributes)
    actions = [
        "add_qnode(key=n0, ids=CHEMBL.COMPOUND:CHEMBL112)",
        "add_qnode(key=n1, categories=biolink:Disease)",
        "add_qedge(key=e01, subject=n0, object=n1, predicates=biolink:treats)",
        "expand(kp=infores:rtx-kg2)",
        "expand(kp=infores:biothings-explorer)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions)
    num_attributes_b = len(nodes_by_qg_id["n0"]["CHEMBL.COMPOUND:CHEMBL112"].attributes)
    assert num_attributes_a == num_attributes_b


@pytest.mark.external
def test_icees_dili():
    actions = [
        "add_qnode(key=n0, ids=NCIT:C28421, categories=biolink:PhenotypicFeature)",
        "add_qnode(key=n1, categories=biolink:NamedThing)",
        "add_qedge(key=e01, subject=n0, object=n1, predicates=biolink:correlated_with)",
        "expand(kp=infores:icees-dili)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions)


@pytest.mark.external
def test_icees_asthma():
    actions = [
        "add_qnode(key=n0, ids=NCIT:C28421, categories=biolink:PhenotypicFeature)",
        "add_qnode(key=n1, categories=biolink:NamedThing)",
        "add_qedge(key=e01, subject=n0, object=n1, predicates=biolink:correlated_with)",
        "expand(kp=infores:icees-asthma)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions)


@pytest.mark.slow
def test_almost_cycle_1565():
    actions_list = [
        "add_qnode(ids=MONDO:0010161, key=n0)",
        "add_qnode(categories=biolink:Gene, key=n1)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n2)",
        "add_qedge(subject=n1, object=n0, key=e0, predicates=biolink:related_to)",
        "add_qedge(subject=n1, object=n2, key=e1, predicates=biolink:related_to)",
        "add_qedge(subject=n0, object=n2, key=e2, predicates=biolink:related_to)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list)


@pytest.mark.slow
def test_fda_approved_query_simple():
    query = {
        "nodes": {
            "n0": {
                "ids": [
                    "MONDO:0000888"
                ]
            },
            "n1": {
                "categories": [
                    "biolink:ChemicalEntity"
                ],
                "constraints": [
                    {
                        "id": "biolink:highest_FDA_approval_status",
                        "name": "highest FDA approval status",
                        "operator": "==",
                        "value": "regular approval"
                    }
                ]
            }
        },
        "edges": {
            "e0": {
                "subject": "n1",
                "object": "n0",
                "predicates": [
                    "biolink:treats"
                ]
            }
        }
    }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query)


@pytest.mark.slow
def test_fda_approved_query_workflow_a9_egfr_advanced():
    query_unconstrained = {
      "nodes": {
        "n0": {
          "categories": [
            "biolink:SmallMolecule"
          ]
        },
        "n1": {
          "ids": [
            "NCBIGene:1956"
          ]
        }
      },
      "edges": {
        "e0": {
          "subject": "n0",
          "object": "n1",
          "predicates": [
            "biolink:decreases_abundance_of",
            "biolink:decreases_activity_of",
            "biolink:decreases_expression_of",
            "biolink:decreases_synthesis_of",
            "biolink:increases_degradation_of",
            "biolink:disrupts",
            "biolink:entity_negatively_regulates_entity"
          ]
        }
      }
    }
    nodes_by_qg_id_unconstrained, edges_by_qg_id_unconstrained = _run_query_and_do_standard_testing(json_query=query_unconstrained)

    query_constrained = query_unconstrained
    fda_approved_constraint = {
        "id": "biolink:highest_FDA_approval_status",
        "name": "highest FDA approval status",
        "operator": "==",
        "value": "regular approval"
    }
    query_constrained["nodes"]["n0"]["constraints"] = [fda_approved_constraint]
    nodes_by_qg_id_constrained, edges_by_qg_id_constrained = _run_query_and_do_standard_testing(json_query=query_constrained)

    assert len(nodes_by_qg_id_constrained["n0"]) < len(nodes_by_qg_id_unconstrained["n0"])


def test_inverted_treats_handling():
    actions = [
        "add_qnode(key=n0, ids=MONDO:0005077)",
        "add_qnode(key=n1, categories=biolink:ChemicalEntity)",
        "add_qedge(key=e0, subject=n0, object=n1, predicates=biolink:treats)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions)


def test_xdtd_expand():
    query = {
            "nodes": {
                "disease": {
                    "ids": ["MONDO:0004975"]
                },
                "chemical": {
                    "categories": ["biolink:ChemicalEntity"]
                }
            },
            "edges": {
                "t_edge": {
                    "object": "disease",
                    "subject": "chemical",
                    "predicates": ["biolink:treats"],
                    "knowledge_type": "inferred"
                }
            }
        }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query)


@pytest.mark.slow
def test_xdtd_different_categories():
    query = {
            "nodes": {
                "disease": {
                    "ids": ["MONDO:0004975"]
                },
                "chemical": {
                    "categories": ["biolink:Drug"]
                }
            },
            "edges": {
                "t_edge": {
                    "object": "disease",
                    "subject": "chemical",
                    "predicates": ["biolink:treats"],
                    "knowledge_type": "inferred"
                }
            }
        }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query)
    query = {
        "nodes": {
            "disease": {
                "ids": ["MONDO:0004975"],
                "categories": ["biolink:Disease"]
            },
            "chemical": {
                "categories": ["biolink:Drug"]
            }
        },
        "edges": {
            "t_edge": {
                "object": "disease",
                "subject": "chemical",
                "predicates": ["biolink:treats"],
                "knowledge_type": "inferred"
            }
        }
    }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query)
    query = {
        "nodes": {
            "disease": {
                "ids": ["MONDO:0004975"],
                "categories": ["biolink:DiseaseOrPhenotypicFeature"]
            },
            "chemical": {
                "categories": ["biolink:ChemicalMixture"]
            }
        },
        "edges": {
            "t_edge": {
                "object": "disease",
                "subject": "chemical",
                "predicates": ["biolink:treats"],
                "knowledge_type": "inferred"
            }
        }
    }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query)


def test_xdtd_multiple_categories():
    query = {
            "nodes": {
                "disease": {
                    "ids": ["MONDO:0004975"]
                },
                "chemical": {
                    "categories": ["biolink:Drug", "biolink:ChemicalMixture"]
                }
            },
            "edges": {
                "t_edge": {
                    "object": "disease",
                    "subject": "chemical",
                    "predicates": ["biolink:treats"],
                    "knowledge_type": "inferred"
                }
            }
        }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query)


def test_xdtd_different_predicates():
    query = {
            "nodes": {
                "disease": {
                    "ids": ["UMLS:C4023597"]
                },
                "chemical": {
                    "categories": ["biolink:Drug", "biolink:ChemicalMixture"]
                }
            },
            "edges": {
                "t_edge": {
                    "object": "disease",
                    "subject": "chemical",
                    "predicates": ["biolink:ameliorates"],
                    "knowledge_type": "inferred"
                }
            }
        }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query)
    query = {
        "nodes": {
            "disease": {
                "ids": ["UMLS:C4023597"]
            },
            "chemical": {
                "categories": ["biolink:Drug", "biolink:ChemicalMixture"]
            }
        },
        "edges": {
            "t_edge": {
                "object": "disease",
                "subject": "chemical",
                "predicates": ["biolink:affects"],
                "knowledge_type": "inferred"
            }
        }
    }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query)


def test_xdtd_no_curies():
    query = {
            "nodes": {
                "disease": {
                },
                "chemical": {
                    "categories": ["biolink:Drug", "biolink:ChemicalMixture"],
                    "ids": ["CHEMBL:CHEMBL1234"]
                }
            },
            "edges": {
                "t_edge": {
                    "object": "disease",
                    "subject": "chemical",
                    "predicates": ["biolink:ameliorates"],
                    "knowledge_type": "inferred"
                }
            }
        }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query, should_throw_error=True)


@pytest.mark.skip
def test_xdtd_with_other_edges():
    query = {
        "nodes": {
            "disease": {
                "ids": ["UMLS:C4023597"]
            },
            "chemical": {
                "categories": ["biolink:Drug", "biolink:ChemicalMixture"]
            },
            "gene": {
                "categories": ["biolink:Gene", "biolink:Protein"]
            }
        },
        "edges": {
            "t_edge": {
                "object": "disease",
                "subject": "chemical",
                "predicates": ["biolink:affects"],
                "knowledge_type": "inferred"
            },
            "non_t_edge": {
                "object": "gene",
                "subject": "chemical"
            }
        }
    }
    #FIXME: this test is failing since the ability to mix inferred with lookup edges is not yet implemented
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query, should_throw_error=True)


def test_xdtd_curie_not_in_db():
    query = {
        "nodes": {
            "disease": {
                "ids": ["MONDO:0021783"]  # this curie has probabilities but no paths in the XDTDdb
            },
            "chemical": {
                "categories": ["biolink:Drug", "biolink:ChemicalMixture"]
            }
        },
        "edges": {
            "t_edge": {
                "object": "disease",
                "subject": "chemical",
                "predicates": ["biolink:affects"],
                "knowledge_type": "inferred"
            }
        }
    }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query, should_throw_error=False)


@pytest.mark.slow
def test_query_ids_mappings():
    query_curies = ["CHEMBL.COMPOUND:CHEMBL112", "DOID:14330"]
    actions_list = [
        f"add_qnode(ids=[{','.join(query_curies)}], key=n00)",
        "add_qnode(categories=biolink:Protein, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:related_to)",
        "expand()",
        "return(message=true, store=false)"
    ]
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(actions_list, timeout=10)
    # Make sure we actually got some subclass child nodes from KPs
    assert len(nodes_by_qg_id["n00"]) > 2
    for node_key, node in nodes_by_qg_id["n00"].items():
        # Make sure pinned nodes have query_ids filled out
        assert node.query_ids or node_key in query_curies
        # Make sure subclass self-edges were added as appropriate
        for parent_query_id in node.query_ids:
            assert parent_query_id in nodes_by_qg_id["n00"]
    # Make sure unpinned nodes do not have query_ids specified
    for node_key, node in nodes_by_qg_id["n01"].items():
        assert not node.query_ids


@pytest.mark.external
def test_no_query_ids_issue():
    query = {
        "nodes": {
            "n1": {
                "categories": [
                    "biolink:GrossAnatomicalStructure"
                ],
                "ids": [
                    "UBERON:0009912",
                    "UBERON:0002535",
                    "UBERON:0000019",
                    "UBERON:0002365",
                    "UBERON:0000017",
                    "UBERON:0000970",
                    "UBERON:0001831",
                    "UBERON:0016410",
                    "UBERON:0001737",
                    "UBERON:0000945"
                ]
            },
            "n2": {
                "categories": [
                    "biolink:Gene"
                ]
            }
        },
        "edges": {
            "e1": {
                "subject": "n1",
                "object": "n2",
                "predicates": [
                    "biolink:expresses"
                ],
                "attribute_constraints": [
                    {
                        "id": "biolink:knowledge_source",
                        "name": "knowledge source",
                        "value": ["infores:connections-hypothesis"],
                        "operator": "==",
                        "not": False
                    }
                ]
            }
        }
    }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query, timeout=45)


@pytest.mark.slow
def test_subclass_answers_for_non_pinned_qnodes():
    query = {
            "nodes": {
                "n0": {
                    "categories": [
                        "biolink:Disease"
                    ],
                    "ids": [
                        "MONDO:0009061"
                    ]
                },
                "n1": {
                    "categories": [
                        "biolink:GrossAnatomicalStructure"
                    ]
                },
                "n2": {
                    "categories": [
                        "biolink:Gene"
                    ]
                },
                "n3": {
                    "categories": [
                        "biolink:Drug",
                        "biolink:SmallMolecule"
                    ]
                }
            },
            "edges": {
                "e0": {
                    "subject": "n0",
                    "object": "n1",
                    "predicates": [
                        "biolink:located_in"
                    ]
                },
                "e1": {
                    "subject": "n1",
                    "object": "n2",
                    "predicates": [
                        "biolink:expresses"
                    ]
                },
                "e2": {
                    "subject": "n3",
                    "object": "n2",
                    "predicates": [
                        "biolink:affects"
                    ]
                }
            }
        }
    nodes_by_qg_id, edges_by_qg_id = _run_query_and_do_standard_testing(json_query=query, timeout=75)


if __name__ == "__main__":
    pytest.main(['-v', 'test_ARAX_expand.py'])


# code/ARAX/test/test_ARAX_filter_kg.py

#!/usr/bin/env python3

# Usage:
# run all: pytest -v test_ARAX_filter_kg.py
# run just certain tests: pytest -v test_ARAX_filter_kg.py -k test_default_std_dev

import sys
import os
import pytest
from collections import Counter
import copy
import json
import ast
from typing import List, Union
import numpy as np

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
from ARAX_filter_kg import ARAXFilterKG
from ARAX_query import ARAXQuery
from ARAX_response import ARAXResponse

PACKAGE_PARENT = '../../UI/OpenAPI/python-flask-server'
sys.path.append(os.path.normpath(os.path.join(os.getcwd(), PACKAGE_PARENT)))
from openapi_server.models.edge import Edge
from openapi_server.models.node import Node
from openapi_server.models.q_edge import QEdge
from openapi_server.models.q_node import QNode
from openapi_server.models.query_graph import QueryGraph
from openapi_server.models.knowledge_graph import KnowledgeGraph
from openapi_server.models.node_binding import NodeBinding
from openapi_server.models.edge_binding import EdgeBinding
from openapi_server.models.result import Result
from openapi_server.models.message import Message


def _do_arax_query(query: dict, print_response: bool=True) -> List[Union[ARAXResponse, Message]]:
    araxq = ARAXQuery()
    response = araxq.query(query)
    if response.status != 'OK' and print_response:
        print(response.show(level=response.DEBUG))
    #return [response, araxq.message]
    return [response, response.envelope.message]

def test_command_definitions():
    fkg = ARAXFilterKG()
    assert fkg.allowable_actions == set(fkg.command_definitions.keys())

def test_warnings():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:8741, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=asdfghjkl, direction=below, threshold=.2)",
            "filter_kg(action=remove_edges_by_discrete_attribute, edge_attribute=asdfghjkl, value=qwertyuiop)",
            "filter_kg(action=remove_edges_by_std_dev, edge_attribute=asdfghjkl, remove_connected_nodes=f, threshold=0.25, top=f, direction=above)",
            "filter_kg(action=remove_edges_by_top_n, edge_attribute=asdfghjkl, remove_connected_nodes=f, n=50, top=f, direction=above)",
            "filter_kg(action=remove_edges_by_percentile, edge_attribute=asdfghjkl, remove_connected_nodes=f, threshold=25, top=f, direction=above)",
            "overlay(action=compute_ngd, virtual_relation_label=N2, subject_qnode_key=n00, object_qnode_key=n01)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=limit_number_of_results, max_results=20)",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) == 20

def test_error():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:1227, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n01, object=n00, key=e00, predicates=biolink:treats)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "filter_kg(action=remove_edges_by_predicate, edge_predicate=biolink:treats, remove_connected_nodes=t, qedge_keys=[e00])",
            "resultify(ignore_edge_direction=true)",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query, False)
    assert response.status == 'ERROR'
    assert response.error_code == "RemovedQueryNode"

def test_edge_key_removal():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:11086, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qnode(categories=biolink:Disease, key=n02)",
            "add_qedge(subject=n01, object=n00, key=e00, predicates=biolink:treats)",
            "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:treats)",
            "expand(kp=infores:rtx-kg2)",
            "filter_kg(action=remove_edges_by_predicate, edge_predicate=biolink:treats, remove_connected_nodes=f, qedge_keys=[e01])",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query, False)
    assert response.status == 'OK'
    edge_key_set = set()
    for edge in message.knowledge_graph.edges.values():
        edge_key_set = edge_key_set.union(edge.qedge_keys)
    assert 'e01' not in edge_key_set

@pytest.mark.slow
def test_default_std_dev():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:0060680, key=n00)",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01)",
        "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J2)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    all_vals = [float(y.value) for x in message.knowledge_graph.edges.values() if x.attributes is not None for y in x.attributes if y.original_attribute_name == 'jaccard_index']
    comp_val = np.mean(all_vals) + np.std(all_vals)
    comp_len = len([x for x in all_vals if x > comp_val])
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:0060680, key=n00)",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01)",
        "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J2)",
        "filter_kg(action=remove_edges_by_std_dev, edge_attribute=jaccard_index, remove_connected_nodes=f)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    vals = [float(y.value) for x in message.knowledge_graph.edges.values() if x.attributes is not None for y in x.attributes if y.original_attribute_name == 'jaccard_index']
    assert len(vals) == comp_len
    assert np.min(vals) > comp_val

@pytest.mark.slow
def test_std_dev():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:0060680, key=n00)",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01)",
        "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J2)",
        "resultify(ignore_edge_direction=true, debug=true)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    all_vals = [float(y.value) for x in message.knowledge_graph.edges.values() if x.attributes is not None for y in x.attributes if y.original_attribute_name == 'jaccard_index']
    assert len(all_vals) > 0
    comp_val = np.mean(all_vals) - 0.25*np.std(all_vals)
    comp_len = len([x for x in all_vals if x < comp_val])
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:0060680, key=n00)",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01)",
        "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J2)",
        "filter_kg(action=remove_edges_by_std_dev, edge_attribute=jaccard_index, remove_connected_nodes=f, threshold=0.25, top=f, direction=above)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    vals = [float(y.value) for x in message.knowledge_graph.edges.values() if x.attributes is not None for y in x.attributes if y.original_attribute_name == 'jaccard_index']
    assert len(vals) == comp_len
    assert len([x for x in vals if x == 1]) == 0
    assert np.max(vals) < comp_val

@pytest.mark.slow
def test_default_top_n():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:0060680, key=n00)",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01)",
        "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J2)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    all_vals = [float(y.value) for x in message.knowledge_graph.edges.values() if x.attributes is not None for y in x.attributes if y.original_attribute_name == 'jaccard_index']
    all_vals.sort()
    all_vals.reverse()
    sorted_vals = all_vals[:50]
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:0060680, key=n00)",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01)",
        "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J2)",
        "filter_kg(action=remove_edges_by_top_n, edge_attribute=jaccard_index, remove_connected_nodes=f)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    vals = [float(y.value) for x in message.knowledge_graph.edges.values() if x.attributes is not None for y in x.attributes if y.original_attribute_name == 'jaccard_index']
    assert len(vals) == 50
    vals.sort()
    vals.reverse()
    assert vals == sorted_vals

def test_remove_property_known_attributes():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(ids=CHEBI:17754, categories=biolink:ChemicalEntity, key=n0)",
        "add_qnode(categories=biolink:Gene, key=n1)",
        "add_qedge(subject=n1, object=n0, key=e0,predicates=biolink:negatively_regulates_entity_to_entity)",
        "expand(kp=infores:rtx-kg2)",
        "filter_kg(action=remove_edges_by_discrete_attribute,edge_attribute=provided_by,value=SEMMEDDB:,remove_connected_nodes=false)",
        "resultify()",
        "filter_results(action=limit_number_of_results, max_results=30)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'

@pytest.mark.slow
def  test_remove_attribute_known_attributes():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:14330, key=n00)",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:physically_interacts_with)",
        "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=jaccard_index, direction=below, threshold=.2, remove_connected_nodes=t, qnode_keys=[n02])",
        #"filter_kg(action=remove_edges_by_discrete_attribute,edge_attribute=provided_by, value=Pharos)",
        "overlay(action=predict_drug_treats_disease, subject_qnode_key=n02, object_qnode_key=n00, virtual_relation_label=P1)",
        "resultify(ignore_edge_direction=true)",
        "filter_results(action=sort_by_edge_attribute, edge_attribute=jaccard_index, direction=descending, max_results=15)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'

@pytest.mark.slow
def test_provided_by_filter():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(ids=CHEBI:17754, categories=biolink:ChemicalEntity, key=n0)",
        "add_qnode(categories=biolink:Gene, key=n1)",
        "add_qedge(subject=n1, object=n0, key=e0,predicates=biolink:entity_negatively_regulates_entity)",
        "expand(kp=infores:rtx-kg2)",
        "filter_kg(action=remove_edges_by_discrete_attribute,edge_attribute=biolink:knowledge_source,value=infores:semmeddb,remove_connected_nodes=false)",
        "resultify()",
        #"filter_results(action=limit_number_of_results, max_results=30)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    count1 = len(message.results)
    assert count1 == 0
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(ids=CHEBI:17754, categories=biolink:ChemicalEntity, key=n0)",
        "add_qnode(categories=biolink:Gene, key=n1)",
        "add_qedge(subject=n1, object=n0, key=e0,predicates=biolink:entity_negatively_regulates_entity)",
        "expand(kp=infores:rtx-kg2)",
        #"filter_kg(action=remove_edges_by_discrete_attribute,edge_attribute=biolink:original_source,value=infores:semmeddb,remove_connected_nodes=false)",
        "resultify()",
        #"filter_results(action=limit_number_of_results, max_results=30)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    count2 = len(message.results)
    assert count2 > count1

@pytest.mark.external
@pytest.mark.slow
def test_stats_error_int_threshold():
    query = {"operations": {"actions": [
        "create_message",
        # Multiple sclerosis -> chemical substance with "related_to" from Clinical Risk KP
        "add_qnode(ids=MONDO:0005301, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0, predicates=biolink:related_to)",
        "expand(kp=infores:biothings-multiomics-clinical-risk,edge_key=e0)",
        "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n0, object_qnode_key=n1)",
        "resultify()",
        "filter_results(action=limit_number_of_results, max_results=10)",
        # Then look for proteins that are shared with these chemical substances and MS
        "add_qnode(categories=biolink:Protein, key=n2, is_set=True)",
        "add_qedge(subject=n0, object=n2, key=e1)",
        "add_qedge(subject=n1, object=n2, key=e2)",
        "expand(edge_key=[e1,e2])",
        # Rank drugs by Jaccard Index
        "overlay(action=compute_jaccard,start_node_key=n0,intermediate_node_key=n2,end_node_key=n1,virtual_relation_label=J1)",
        "filter_kg(action=remove_edges_by_top_n,edge_attribute=jaccard_index, n=10,remove_connected_nodes=true,qnode_keys=[n2])",
        "overlay(action=compute_ngd, virtual_relation_label=N2, subject_qnode_key=n1, object_qnode_key=n2)",
        "overlay(action=compute_ngd, virtual_relation_label=N3, subject_qnode_key=n0, object_qnode_key=n2)",
        "resultify()",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'

def test_tuple_bug():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(key=n00,ids=DRUGBANK:DB00150,categories=biolink:ChemicalEntity)",
        "add_qnode(key=n01,categories=biolink:Protein)",
        "add_qedge(key=e00,subject=n00,object=n01)",
        "expand(edge_key=e00, kp=infores:rtx-kg2)",
        "overlay(action=fisher_exact_test,subject_qnode_key=n00,virtual_relation_label=F0,object_qnode_key=n01)",
        "filter_kg(action=remove_edges_by_top_n,edge_attribute=fisher_exact_test_p-value,direction=below,n=10,remove_connected_nodes=true,qnode_keys=[n01])",
        "resultify()",
        "filter_results(action=limit_number_of_results, max_results=100)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'

if __name__ == "__main__":
    pytest.main(['-v'])


# code/ARAX/test/test_ARAX_filter_results.py

#!/usr/bin/env python3

# Usage:
# run all: pytest -v test_ARAX_filter_results.py
# run just certain tests: pytest -v test_ARAX_filter_results.py -k test_sort

import sys
import os
import pytest
from collections import Counter
import copy
import json
import ast
from typing import List, Union

sys.path.append(os.getcwd()+"/../ARAXQuery")
#sys.path.append(os.getcwd()+"/../ARAXQuery")
from ARAX_filter_results import ARAXFilterResults
from ARAX_query import ARAXQuery
from ARAX_response import ARAXResponse

PACKAGE_PARENT = '../../UI/OpenAPI/python-flask-server'
sys.path.append(os.path.normpath(os.path.join(os.getcwd(), PACKAGE_PARENT)))
from openapi_server.models.edge import Edge
from openapi_server.models.node import Node
from openapi_server.models.q_edge import QEdge
from openapi_server.models.q_node import QNode
from openapi_server.models.query_graph import QueryGraph
from openapi_server.models.knowledge_graph import KnowledgeGraph
from openapi_server.models.node_binding import NodeBinding
from openapi_server.models.edge_binding import EdgeBinding
from openapi_server.models.result import Result
from openapi_server.models.message import Message


def _do_arax_query(query: dict) -> List[Union[ARAXResponse, Message]]:
    araxq = ARAXQuery()
    response = araxq.query(query)
    if response.status != 'OK':
        print(response.show(level=response.DEBUG))
    #return [response, araxq.message]
    return [response, response.envelope.message]

def test_command_definitions():
    fr = ARAXFilterResults()
    assert fr.allowable_actions == set(fr.command_definitions.keys())

def test_n_results():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:4337, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "overlay(action=add_node_pmids, max_num=15)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_node_attribute, node_attribute=pubmed_ids, direction=a, max_results=20)",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert message.n_results == len(message.results) == 20

def test_no_results():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:4337, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "overlay(action=add_node_pmids, max_num=15)",
            "filter_results(action=sort_by_node_attribute, node_attribute=pubmed_ids, direction=a, max_results=20)",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert 'WARNING: [] filter_results called with no results.' in response.show(level=ARAXResponse.WARNING)
    assert response.status == 'OK'

@pytest.mark.slow
def test_prune():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:4337, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "overlay(action=add_node_pmids, max_num=15)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_node_attribute, node_attribute=pubmed_ids, direction=a, max_results=20, prune_kg=f)",
            "return(message=true, store=false)"
        ]}}
    [no_prune_response, no_prune_message] = _do_arax_query(query)
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:4337, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "overlay(action=add_node_pmids, max_num=15)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_node_attribute, node_attribute=pubmed_ids, direction=a, max_results=20)",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    result_nodes = set()
    result_edges = set()
    for result in message.results:
        for node_binding_list in result.node_bindings.values():
            for node_binding in node_binding_list:
                result_nodes.add(node_binding.id)
        for edge_binding_list in result.edge_bindings.values():
            for edge_binding in edge_binding_list:
                result_edges.add(edge_binding.id)
    for key, node in message.knowledge_graph.nodes.items():
        assert key in result_nodes
    for key, edge in message.knowledge_graph.edges.items():
        assert key in result_edges
    assert len(message.knowledge_graph.nodes) < len(no_prune_message.knowledge_graph.nodes)
    assert len(message.knowledge_graph.edges) < len(no_prune_message.knowledge_graph.edges)

def test_warning():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:4337, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "overlay(action=add_node_pmids, max_num=15)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_node_attribute, node_attribute=pubmed_ids, direction=a, max_results=20)",
            "filter_results(action=sort_by_node_attribute, node_attribute=asdfghjkl, direction=a, max_results=20)",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) == 20

@pytest.mark.slow
def test_sort_by_edge_attribute():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:0060680, key=n00)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01)",
            "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J2)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_edge_attribute, edge_attribute=jaccard_index, direction=d, max_results=20, qedge_keys=[J2])",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    #return response, message
    assert response.status == 'OK'
    assert len(message.results) == 20

def test_sort_by_node_attribute():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:4337, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "overlay(action=add_node_pmids, max_num=15)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_node_attribute, node_attribute=pubmed_ids, direction=a, max_results=20, qnode_keys=[n01])",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) == 20
    # add something to test if the results are assending and the correct numbers

def test_sort_by_score():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:4337, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00, kp=infores:rtx-kg2)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_score, direction=a, max_results=20)",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) == 20
    result_scores = [x.score for x in message.results]
    assert result_scores == sorted(result_scores)
    assert max(result_scores) < 1

@pytest.mark.external
def test_issue1506():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(ids=MONDO:0005301, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n01, object=n00, key=e00, predicates=biolink:related_to)",
            "expand(kp=infores:biothings-multiomics-clinical-risk, edge_key=e00)",
            "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n01, object_qnode_key=n00)",
            "resultify()",
            "filter_results(action=sort_by_edge_attribute, edge_attribute=feature_coefficient, direction=descending, max_results=30, prune_kg=true)",
            "return(message=true, store=false)"
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) == 30


if __name__ == "__main__":
    pytest.main(['-v'])


# code/ARAX/test/test_ARAX_infer.py

#!/usr/bin/env python3

# Intended to test ARAX infer

import sys
import os
import pytest
from collections import Counter
import copy
import json
import ast
from typing import List, Union

import numpy as np

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../ARAXQuery")
sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
from ARAX_query import ARAXQuery
from ARAX_response import ARAXResponse

PACKAGE_PARENT = '../../UI/OpenAPI/openapi_server'
sys.path.append(os.path.normpath(os.path.join(os.getcwd(), PACKAGE_PARENT)))
from openapi_server.models.message import Message


def _do_arax_query(query: dict) -> List[Union[ARAXResponse, Message]]:
    araxq = ARAXQuery()
    response = araxq.query(query)
    if response.status != 'OK':
        print(response.show(level=response.DEBUG))
    return [response, response.envelope.message]


def _attribute_tester(message, attribute_name: str, attribute_type: str, num_different_values=2):
    """
    Tests attributes of a message
    message: returned from _do_arax_query
    attribute_name: the attribute name to test (eg. 'jaccard_index')
    attribute_type: the attribute type (eg. 'EDAM:data_1234')
    num_different_values: the number of distinct values you wish to see have been added as attributes
    """
    edges_of_interest = []
    values = set()
    for key, edge in message.knowledge_graph.edges.items():
        if hasattr(edge, 'edge_attributes'):
            for attr in edge.edge_attributes:
                if attr.original_attribute_name == attribute_name:
                    edges_of_interest.append(edge)
                    assert attr.attribute_type_id == attribute_type
                    values.add(attr.value)
    assert len(edges_of_interest) > 0
    assert len(values) >= num_different_values


def _virtual_tester(message: Message, edge_predicate: str, relation: str, attribute_name: str, attribute_type: str, num_different_values=2):
    """
    Tests overlay functions that add virtual edges
    message: returned from _do_arax_query
    edge_predicate: the name of the virtual edge (eg. biolink:has_jaccard_index_with)
    relation: the relation you picked for the virtual_edge_relation (eg. N1)
    attribute_name: the attribute name to test (eg. 'jaccard_index')
    attribute_type: the attribute type (eg. 'EDAM:data_1234')
    num_different_values: the number of distinct values you wish to see have been added as attributes
    """
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert edge_predicate in edge_predicates_in_kg
    edges_of_interest = [x for x in message.knowledge_graph.edges.values() if x.relation == relation]
    values = set()
    assert len(edges_of_interest) > 0
    for edge in edges_of_interest:
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        assert edge.attributes[0].original_attribute_name == attribute_name
        values.add(edge.attributes[0].value)
        assert edge.attributes[0].attribute_type_id == attribute_type
    # make sure two or more values were added
    assert len(values) >= num_different_values


def test_infer_alkaptonuria():
    query = {"operations": {"actions": [
            "create_message",
            "infer(action=drug_treatment_graph_expansion,node_curie=MONDO:0008753)",
            "return(message=true, store=true)"
        ]}}
    [response, message] = _do_arax_query(query)
    # return response, message
    assert response.status == 'OK'
    assert len(message.query_graph.edges) > 1
    assert len(message.results) > 0


def test_with_qg():
    query = {
        "message": {"query_graph": {
            "nodes": {
                "disease": {
                    "ids": ["MONDO:0004975"]
                },
                "chemical": {
                    "categories": ["biolink:ChemicalEntity"]
                }
            },
            "edges": {
                "t_edge": {
                    "object": "disease",
                    "subject": "chemical",
                    "predicates": ["biolink:treats"],
                    "knowledge_type": "inferred"
                }
            }
        }
        },
        "operations": {"actions": [
            "infer(action=drug_treatment_graph_expansion,node_curie=MONDO:0008753,qedge_id=t_edge)",
            "return(message=true, store=true)"
        ]}
    }
    [response, message] = _do_arax_query(query)
    # return response, message
    assert response.status == 'ERROR'
    #assert len(message.query_graph.edges) > 1
    #assert len(message.results) > 0


def test_with_qg2():
    query = {
        "message": {"query_graph": {
            "nodes": {
                "disease": {
                    "ids": ["MONDO:0004975"]
                },
                "chemical": {
                    "categories": ["biolink:ChemicalEntity"]
                }
            },
            "edges": {
                "t_edge": {
                    "object": "disease",
                    "subject": "chemical",
                    "predicates": ["biolink:treats"],
                    "knowledge_type": "inferred"
                }
            }
        }
        },
        "operations": {"actions": [
            "infer(action=drug_treatment_graph_expansion,node_curie=MONDO:0004975,qedge_id=t_edge)",
            "return(message=true, store=true)"
        ]}
    }
    [response, message] = _do_arax_query(query)
    # return response, message
    assert response.status == 'OK'
    assert len(message.query_graph.edges) > 1
    assert len(message.results) > 0


def test_with_only_qg():
    query = {
        "message": {"query_graph": {
            "nodes": {
                "disease": {
                    "ids": ["MONDO:0004975"]
                },
                "chemical": {
                    "categories": ["biolink:ChemicalEntity"]
                }
            },
            "edges": {
                "t_edge": {
                    "object": "disease",
                    "subject": "chemical",
                    "predicates": ["biolink:treats"],
                    "knowledge_type": "inferred"
                }
            }
        }
        }
    }
    [response, message] = _do_arax_query(query)
    # return response, message
    assert response.status == 'OK'
    assert len(message.query_graph.edges) > 1
    assert len(message.results) > 0




# code/ARAX/test/test_ARAX_json_queries.py

#!/usr/bin/env python3
# For testing the ARAX json queries with things like the query graph interpreter
import sys
import os
import pytest
from typing import List, Dict, Tuple, Optional

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery/")
from ARAX_query import ARAXQuery
from ARAX_response import ARAXResponse
import Expand.expand_utilities as eu
sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../UI/OpenAPI/python-flask-server/")
from openapi_server.models.edge import Edge
from openapi_server.models.node import Node
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from test_ARAX_expand import check_node_categories, check_property_format, check_for_orphans, print_nodes, print_edges, print_counts_by_qgid


def _run_query_and_do_standard_testing(actions: Optional[List[str]] = None, json_query: Optional[dict] = None,
                                       kg_should_be_incomplete=False, debug=False, should_throw_error=False,
                                       error_code: Optional[str] = None, timeout: Optional[int] = None) -> Tuple[Dict[str, Dict[str, Node]], Dict[str, Dict[str, Edge]], ARAXResponse]:
    # Run the query
    araxq = ARAXQuery()
    assert actions or json_query  # Must provide some sort of query to run
    # Stick the actions in if they are provided
    if actions:
        query_object = {"operations": {"actions": actions}}
    # otherwise check if it's just the query_graph element
    elif "message" not in json_query:
        query_object = {"message": {"query_graph": json_query}}
    else:
        query_object = json_query
    if timeout:
        query_object["query_options"] = {"kp_timeout": timeout}
    response = araxq.query(query_object)
    message = araxq.message
    if response.status != 'OK':
        print(response.show(level=ARAXResponse.DEBUG))
    assert response.status == 'OK' or should_throw_error
    if should_throw_error and error_code:
        assert response.error_code == error_code

    # Convert output knowledge graph to a dictionary format for faster processing (organized by QG IDs)
    dict_kg = eu.convert_standard_kg_to_qg_organized_kg(message.knowledge_graph)
    nodes_by_qg_id = dict_kg.nodes_by_qg_id
    edges_by_qg_id = dict_kg.edges_by_qg_id

    # Optionally print more detail
    if debug:
        print_nodes(nodes_by_qg_id)
        print_edges(edges_by_qg_id)
        print_counts_by_qgid(nodes_by_qg_id, edges_by_qg_id)
        print(response.show(level=ARAXResponse.DEBUG))

    # Run standard testing (applies to every test case)
    assert eu.qg_is_fulfilled(message.query_graph, dict_kg, enforce_required_only=True) or kg_should_be_incomplete or should_throw_error
    check_for_orphans(nodes_by_qg_id, edges_by_qg_id)
    check_property_format(nodes_by_qg_id, edges_by_qg_id)
    check_node_categories(message.knowledge_graph.nodes, message.query_graph)

    return nodes_by_qg_id, edges_by_qg_id, response


def test_query_by_query_graph_2():
    query = { "message": { "query_graph": { "edges": {
                "qg2": { "subject": "qg1", "object": "qg0", "predicates": ["biolink:physically_interacts_with"] }
            },
            "nodes": {
                "qg0": { "name": "acetaminophen", "ids": ["CHEMBL.COMPOUND:CHEMBL112"], "categories": ["biolink:ChemicalEntity"] },
                "qg1": { "name": None, "ids": None, "categories": ["biolink:Protein"] }
            } } } }
    #araxq = ARAXQuery()
    #araxq.query(query)
    #response = araxq.response
    #print(response.show())
    nodes_by_qg_id, edges_by_qg_id, response = _run_query_and_do_standard_testing(json_query=query)
    #assert response.status == 'OK'
    #message = response.envelope.message
    #assert len(message.results) >= 20
    #assert response.envelope.schema_version == '1.2.0'


def test_ngd_added():
    """
    Test that the NGD added property is set correctly and was added by the QGI
    """
    query = {
        "edges": {
            "e00": {
                "subject": "n00",
                "object": "n01",
                "predicates": ["biolink:physically_interacts_with"]
            }
        },
        "nodes": {
            "n00": {
                "ids": ["CHEMBL.COMPOUND:CHEMBL112"]
            },
            "n01": {
                "categories": ["biolink:Protein"]
            }
        }
    }
    nodes_by_qg_id, edges_by_qg_id, response = _run_query_and_do_standard_testing(json_query=query)
    qg = response.envelope.message.query_graph
    assert 'N1' in qg.edges
    assert 'biolink:has_normalized_google_distance_with' in qg.edges['N1'].predicates


def test_drug_disease_query():
    query = {
        "edges": {
            "e00": {
                "subject": "n00",
                "object": "n01"
            }
        },
        "nodes": {
            "n00": {
                "ids": ["MONDO:0021783"],
                "categories": ["biolink:Disease"]
            },
            "n01": {
                "categories": ["biolink:ChemicalEntity"]
            }
        }
    }
    nodes_by_qg_id, edges_by_qg_id, response = _run_query_and_do_standard_testing(json_query=query)
    qg = response.envelope.message.query_graph
    assert 'N1' in qg.edges
    assert 'biolink:has_normalized_google_distance_with' in qg.edges['N1'].predicates


def test_workflow1():
    """
    Test a fill (with one KP), bind, score workflow
    """
    query = {
        "workflow": [
            {
                "id": "fill",
                "parameters": {
                    "allowlist": [
                        "infores:rtx-kg2"
                    ],
                    "qedge_keys": [
                        "e00"
                    ]
                }
            },
            {
                "id": "bind"
            },
            {
                "id": "score"
            }
        ],
        "message": {
            "query_graph": {
                "edges": {
                    "e00": {
                        "subject": "n00",
                        "object": "n01",
                        "predicates": [
                            "biolink:physically_interacts_with"
                        ]
                    }
                },
                "nodes": {
                    "n00": {
                        "ids": [
                            "CHEMBL.COMPOUND:CHEMBL112"
                        ]
                    },
                    "n01": {
                        "categories": [
                            "biolink:Protein"
                        ]
                    }
                }
            }
        }
    }
    nodes_by_qg_id, edges_by_qg_id, response = _run_query_and_do_standard_testing(json_query=query)
    essences = [x.to_dict()['essence'] for x in response.envelope.message.results]
    assert 'METICRANE' in essences

@pytest.mark.slow
def test_workflow2():
    """
    Every possible combination of allowlist and qedge_keys
    """
    query = {
        "workflow": [
            {
                "id": "fill",
                "parameters": {
                    "allowlist": [
                        "infores:rtx-kg2",
                        "infores:biothings-explorer"
                    ],
                    "qedge_keys": [
                        "e0"
                    ]
                }
            },
            {
                "id": "fill",
                "parameters": {
                    "qedge_keys": [
                        "e0"
                    ]
                }
            },
            {
                "id": "fill",
                "parameters": {
                    "allowlist": [
                        "infores:rtx-kg2",
                        "infores:biothings-explorer"
                    ]
                }
            },
            {
                "id": "fill",
                "parameters": {
                    "allowlist": [
                        "infores:connections-hypothesis"
                    ],
                    "qedge_keys": [
                        "e1",
                        "e2"
                    ]
                }
            },
            {
                "id": "fill",
                "parameters": {
                    "allowlist": [
                        "infores:rtx-kg2",
                        "infores:biothings-explorer"
                    ],
                    "qedge_keys": [
                        "e2"
                    ]
                }
            },
            {
                "id": "bind"
            },
            {
                "id": "complete_results"
            },
            {
                "id": "score"
            }
        ],
        "message": {
            "query_graph": {
                "nodes": {
                    "n0": {
                        "categories": [
                            "biolink:Disease"
                        ],
                        "ids": [
                            "MONDO:0009061"
                        ]
                    },
                    "n1": {
                        "categories": [
                            "biolink:GrossAnatomicalStructure"
                        ]
                    },
                    "n2": {
                        "categories": [
                            "biolink:Gene"
                        ]
                    },
                    "n3": {
                        "categories": [
                            "biolink:Drug",
                            "biolink:SmallMolecule"
                        ]
                    }
                },
                "edges": {
                    "e0": {
                        "subject": "n0",
                        "object": "n1",
                        "predicates": [
                            "biolink:related_to"
                        ]
                    },
                    "e1": {
                        "subject": "n1",
                        "object": "n2",
                        "predicates": [
                            "biolink:expresses"
                        ]
                    },
                    "e2": {
                        "subject": "n3",
                        "object": "n2",
                        "predicates": [
                            "biolink:affects"
                        ]
                    }
                }
            }
        }
    }
    nodes_by_qg_id, edges_by_qg_id, response = _run_query_and_do_standard_testing(json_query=query)
    assert response.status == 'OK'
    essences = [x.to_dict()['essence'] for x in response.envelope.message.results]

if __name__ == "__main__":
    pytest.main(['-v', 'test_ARAX_json_queries.py'])






# code/ARAX/test/test_ARAX_messenger.py

#!/usr/bin/env python3

import sys
import os
import pytest

import copy
import json
import ast

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
from ARAX_messenger import ARAXMessenger
from ARAX_response import ARAXResponse


def test_create_message_basic():
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    assert response.status == 'OK'
    message = response.envelope.message
    assert response.envelope.type == 'translator_reasoner_response'
    assert response.envelope.schema_version == '1.3.0'


def test_create_message_node_edge_types():
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    assert response.status == 'OK'
    message = response.envelope.message
    assert isinstance(message.knowledge_graph.nodes, dict)
    assert isinstance(message.knowledge_graph.edges, dict)
    assert isinstance(message.query_graph.nodes, dict)
    assert isinstance(message.query_graph.edges, dict)


def test_add_qnode_basic():
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    assert response.status == 'OK'
    message = response.envelope.message
    messenger.add_qnode(response,{})
    assert response.status == 'OK'
    assert isinstance(message.query_graph.nodes, dict)
    assert len(message.query_graph.nodes) == 1
    assert message.query_graph.nodes['n00'].ids == None


def test_add_qnode_curie_scalar():
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    assert response.status == 'OK'
    message = response.envelope.message
    messenger.add_qnode(response,{ 'ids': ['UniProtKB:P14136'] })
    assert response.status == 'OK'
    assert isinstance(message.query_graph.nodes, dict)
    assert len(message.query_graph.nodes) == 1
    assert len(message.query_graph.nodes['n00'].ids) == 1


def test_add_qnode_curie_list():
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    assert response.status == 'OK'
    message = response.envelope.message
    messenger.add_qnode(response,{ 'ids': ['UniProtKB:P14136','UniProtKB:P35579'] })
    assert response.status == 'OK'
    assert isinstance(message.query_graph.nodes, dict)
    assert len(message.query_graph.nodes) == 1
    assert len(message.query_graph.nodes['n00'].ids) == 2


def test_add_qnode_name():
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    assert response.status == 'OK'
    message = response.envelope.message
    messenger.add_qnode(response,{ 'name': 'acetaminophen' })
    assert response.status == 'OK'
    assert isinstance(message.query_graph.nodes, dict)
    assert len(message.query_graph.nodes) == 1
    assert message.query_graph.nodes['n00'].ids[0] == 'CHEMBL.COMPOUND:CHEMBL112'


def test_add_qnode_type():
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    assert response.status == 'OK'
    message = response.envelope.message
    messenger.add_qnode(response,{ 'categories': ['biolink:Protein'] })
    assert response.status == 'OK'
    assert isinstance(message.query_graph.nodes, dict)
    assert len(message.query_graph.nodes) == 1
    assert message.query_graph.nodes['n00'].categories[0] == 'biolink:Protein'


def test_add_qnode_group_id_is_set_false():
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    assert response.status == 'OK'
    message = response.envelope.message
    messenger.add_qnode(response,{ 'categories': ['biolink:Protein'], 'is_set' : 'false', 'option_group_id' : '0' })
    assert response.status == 'ERROR'
    assert isinstance(message.query_graph.nodes, dict)
    assert len(message.query_graph.nodes) == 0
    assert response.error_code == 'InputMismatch'


def test_add_qnode_bad_name():
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    assert response.status == 'OK'
    message = response.envelope.message
    messenger.add_qnode(response,{ 'name': 'Big Bird' })
    assert response.status == 'ERROR'
    assert isinstance(message.query_graph.nodes, dict)
    assert len(message.query_graph.nodes) == 0
    assert response.error_code == 'UnresolvableNodeName'


def test_add_qnode_duplicate_key():
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    assert response.status == 'OK'
    message = response.envelope.message
    messenger.add_qnode(response, { 'key': 'n00', 'ids': [ 'CHEMBL.COMPOUND:CHEMBL112' ] } )
    assert response.status == 'OK'
    messenger.add_qnode(response, { 'key': 'n00', 'ids': [ 'CHEBI:46195' ] } )
    print(json.dumps(ast.literal_eval(repr(message.query_graph.nodes)), sort_keys=True, indent=2))
    assert response.status == 'ERROR'
    assert isinstance(message.query_graph.nodes, dict)
    assert len(message.query_graph.nodes) == 1
    assert response.error_code == 'QNodeDuplicateKey'


def test_add_qedge_duplicate_key():
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    assert response.status == 'OK'
    message = response.envelope.message
    messenger.add_qnode(response, { 'key': 'n00', 'ids': [ 'CHEMBL.COMPOUND:CHEMBL112' ] } )
    messenger.add_qnode(response, { 'key': 'n01', 'categories': [ 'biolink:Protein' ] } )
    messenger.add_qedge(response, { 'key': 'e00', 'subject': 'n00', 'object': 'n01' } )
    assert response.status == 'OK'
    messenger.add_qedge(response, { 'key': 'e00', 'subject': 'n00', 'object': 'n01', 'predicates': [ 'biolink:treats' ] } )
    print(json.dumps(ast.literal_eval(repr(message.query_graph.edges)), sort_keys=True, indent=2))
    assert response.status == 'ERROR'
    assert isinstance(message.query_graph.nodes, dict)
    assert len(message.query_graph.edges) == 1
    assert response.error_code == 'QEdgeDuplicateKey'


def test_add_qnode_bad_parameters():
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    assert response.status == 'OK'
    bad_parameters_list = [
        { 'parameters': [ 'ids', 'PICKLES:123' ], 'error_code': 'ParametersNotDict' },
        { 'parameters': { 'pickles': 'on the side' }, 'error_code': 'UnknownParameter' },
        { 'parameters': { 'ids': 'n2', 'category': 'biolink:Disease' }, 'error_code': 'UnknownParameter' },
    ]
    template_response = copy.deepcopy(response)
    for bad_parameters in bad_parameters_list:
        response = copy.deepcopy(template_response)
        message = response.envelope.message
        print(bad_parameters)
        messenger.add_qnode(response, bad_parameters['parameters'])
        assert response.status == 'ERROR'
        assert len(message.query_graph.nodes) == 0
        assert response.error_code == bad_parameters['error_code']


def test_add_qedge_multitest():
    # Set up a message with two nodes
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    assert response.status == 'OK'
    message = response.envelope.message
    messenger.add_qnode(response,{ 'name': 'acetaminophen' })
    assert response.status == 'OK'
    messenger.add_qnode(response,{ 'categories': ['biolink:Protein'] })
    assert response.status == 'OK'

    # Set up a list of parameters to feed to add_qedge() and what the result should be
    parameters_list = [
        { 'status': 'ERROR', 'parameters': [ 'subject', 'n00' ], 'error_code': 'ParametersNotDict' },
        { 'status': 'OK', 'parameters': { 'subject': 'n00', 'object': 'n01' }, 'error_code': 'OK' },
        { 'status': 'OK', 'parameters': { 'subject': 'n00', 'object': 'n01', 'key': 'e99' }, 'error_code': 'OK' },
        { 'status': 'OK', 'parameters': { 'subject': 'n00', 'object': 'n01', 'key': 'e99', 'predicates': ['biolink:physically_interacts_with'] }, 'error_code': 'OK' },
        { 'status': 'ERROR', 'parameters': { 'subject': 'n00' }, 'error_code': 'MissingTargetKey' },
        { 'status': 'ERROR', 'parameters': { 'object': 'n00' }, 'error_code': 'MissingSourceKey' },
        { 'status': 'ERROR', 'parameters': { 'subject': 'n99', 'object': 'n01' }, 'error_code': 'UnknownSourceKey' },
        { 'status': 'ERROR', 'parameters': { 'subject': 'n00', 'object': 'n99' }, 'error_code': 'UnknownTargetKey' },
        { 'status': 'ERROR', 'parameters': { 'pickles': 'on the side' }, 'error_code': 'UnknownParameter' },
    ]

    # Loop over all the parameter sets and try to run it
    template_response = copy.deepcopy(response)
    for parameters in parameters_list:
        response = copy.deepcopy(template_response)
        message = response.envelope.message
        print(parameters)
        messenger.add_qedge(response, parameters['parameters'])
        assert response.status == parameters['status']
        if parameters['status'] == 'OK':
            assert len(message.query_graph.edges) == 1
            continue
        assert len(message.query_graph.edges) == 0
        assert response.error_code == parameters['error_code']


if __name__ == "__main__": pytest.main(['-v'])


# code/ARAX/test/test_ARAX_overlay.py

#!/usr/bin/env python3

# Usage:
# run all: pytest -v test_ARAX_overlay.py
# run just certain tests: pytest -v test_ARAX_overlay.py -k test_jaccard

import sys
import os
import pytest
from collections import Counter
import copy
import json
import ast
from typing import List, Union

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
from ARAX_query import ARAXQuery
from ARAX_response import ARAXResponse

PACKAGE_PARENT = '../../UI/OpenAPI/python-flask-server'
sys.path.append(os.path.normpath(os.path.join(os.getcwd(), PACKAGE_PARENT)))
from openapi_server.models.edge import Edge
from openapi_server.models.node import Node
from openapi_server.models.q_edge import QEdge
from openapi_server.models.q_node import QNode
from openapi_server.models.query_graph import QueryGraph
from openapi_server.models.knowledge_graph import KnowledgeGraph
from openapi_server.models.node_binding import NodeBinding
from openapi_server.models.edge_binding import EdgeBinding
from openapi_server.models.result import Result
from openapi_server.models.message import Message


def _do_arax_query(query: dict) -> List[Union[ARAXResponse, Message]]:
    araxq = ARAXQuery()
    response = araxq.query(query)
    if response.status != 'OK':
        print(response.show(level=response.DEBUG))
    #return [response, araxq.message]
    return [response, response.envelope.message]


def _attribute_tester(message, attribute_name: str, attribute_type: str, num_different_values=2, num_edges_of_interest=1):
    """
    Tests attributes of a message
    message: returned from _do_arax_query
    attribute_name: the attribute name to test (eg. 'jaccard_index')
    attribute_type: the attribute type (eg. 'EDAM:data_1234')
    num_different_values: the number of distinct values you wish to see have been added as attributes
    num_edges_of_interest: the minimum number of edges in the KG you wish to see have the attribute of interest
    """
    edges_of_interest = []
    values = set()
    for edge in message.knowledge_graph.edges.values():
        if hasattr(edge, 'attributes') and edge.attributes:
            for attr in edge.attributes:
                if attr.original_attribute_name == attribute_name:
                    edges_of_interest.append(edge)
                    assert attr.attribute_type_id == attribute_type
                    values.add(attr.value)
    assert len(edges_of_interest) >= num_edges_of_interest
    if edges_of_interest:
        assert len(values) >= num_different_values


def _virtual_tester(message: Message, edge_predicate: str, relation: str, attribute_name: str, attribute_type: str,
                    num_different_values=2, num_edges_of_interest=1):
    """
    Tests overlay functions that add virtual edges
    message: returned from _do_arax_query
    edge_predicate: the name of the virtual edge (eg. biolink:has_jaccard_index_with)
    relation: the relation you picked for the virtual_edge_relation (eg. N1)
    attribute_name: the attribute name to test (eg. 'jaccard_index')
    attribute_type: the attribute type (eg. 'EDAM:data_1234')
    num_different_values: the number of distinct values you wish to see have been added as attributes
    num_edges_of_interest: the minimum number of virtual edges you wish to see have been added to the KG
    """
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert edge_predicate in edge_predicates_in_kg
    edges_of_interest = []
    for edge in message.knowledge_graph.edges.values():
        add_edge = False
        for attribute in edge.attributes:
            if attribute.original_attribute_name == "virtual_relation_label":
                if attribute.value == relation:
                    add_edge = True
        if add_edge:
            edges_of_interest.append(edge)
    #edges_of_interest = [x for x in message.knowledge_graph.edges.values() if x.relation == relation]
    assert len(edges_of_interest) >= num_edges_of_interest
    if edges_of_interest:
        values = set()
        for edge in edges_of_interest:
            assert hasattr(edge, 'attributes')
            assert edge.attributes
            assert edge.attributes[0].original_attribute_name == attribute_name
            values.add(edge.attributes[0].value)
            assert edge.attributes[0].attribute_type_id == attribute_type
        # make sure two or more values were added
        assert len(values) >= num_different_values


def test_jaccard():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:1947, key=n00)",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:physically_interacts_with)",
        "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
        "resultify(ignore_edge_direction=true, debug=true)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert 'biolink:has_jaccard_index_with' in edge_predicates_in_kg
    jaccard_edges = []
    for edge in message.knowledge_graph.edges.values():
        add_edge = False
        for attribute in edge.attributes:
            if attribute.original_attribute_name == "virtual_relation_label":
                if attribute.value == "J1":
                    add_edge = True
        if add_edge:
            jaccard_edges.append(edge)
    assert len(jaccard_edges) > 0
    for edge in jaccard_edges:
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        assert edge.attributes[0].original_attribute_name == 'jaccard_index'
        assert edge.attributes[0].value >= 0


def test_add_node_pmids():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=MONDO:0018077, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(edge_key=e00, kp=infores:rtx-kg2)",
        "overlay(action=add_node_pmids, max_num=15)",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    # check response status
    assert response.status == 'OK'
    # check if there are nodes with attributes
    nodes_with_attributes = [x for x in message.knowledge_graph.nodes.values() if hasattr(x, 'attributes')]
    assert len(nodes_with_attributes) > 0
    # check if pmids were added
    nodes_with_pmids = []
    for node in nodes_with_attributes:
        for attr in node.attributes:
            if attr.original_attribute_name == 'pubmed_ids':
                nodes_with_pmids.append(node)
    assert len(nodes_with_pmids) > 0
    # check types
    for node in nodes_with_pmids:
        for attr in node.attributes:
            if attr.original_attribute_name == "pubmed_ids":
                assert attr.attribute_type_id == 'EDAM:data_0971'
                assert attr.value.__class__ == list


def test_compute_ngd_virtual():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:384, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(edge_key=e00, kp=infores:rtx-kg2)",
        "overlay(action=compute_ngd, subject_qnode_key=n00, object_qnode_key=n01, virtual_relation_label=N1)",
        "resultify(ignore_edge_direction=true, debug=true)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert 'biolink:has_normalized_google_distance_with' in edge_predicates_in_kg
    ngd_edges = []
    for edge in message.knowledge_graph.edges.values():
        add_edge = False
        for attribute in edge.attributes:
            if attribute.original_attribute_name == "virtual_relation_label":
                if attribute.value == "N1":
                    add_edge = True
        if add_edge:
            ngd_edges.append(edge)
    assert len(ngd_edges) > 0
    for edge in ngd_edges:
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        attribute_names = {attribute.original_attribute_name: attribute.value for attribute in edge.attributes}
        assert "publications" in attribute_names
        assert len(attribute_names["publications"]) <= 30
        assert edge.attributes[0].original_attribute_name == 'normalized_google_distance'
        assert float(edge.attributes[0].value) >= 0


def test_compute_ngd_attribute():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:384, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(edge_key=e00, kp=infores:rtx-kg2)",
        "overlay(action=compute_ngd)",
        "resultify(ignore_edge_direction=true, debug=true)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    ngd_edges = []
    for edge in message.knowledge_graph.edges.values():
        if hasattr(edge, 'attributes'):
            for attr in edge.attributes:
                if attr.original_attribute_name == 'normalized_google_distance':
                    ngd_edges.append(edge)
                    assert float(attr.value) >= 0
                    assert attr.attribute_type_id == 'EDAM:data_2526'
    assert len(ngd_edges) > 0
    for edge in ngd_edges:
        attribute_names = {attribute.original_attribute_name: attribute.value for attribute in edge.attributes}
        assert "ngd_publications" in attribute_names
        assert len(attribute_names["ngd_publications"]) <= 30


def test_FET_ex1():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(ids=DOID:12889, key=n00, categories=biolink:Disease)",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
        "add_qedge(subject=n00, object=n01,key=e00)",
        "expand(edge_key=e00, kp=infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n00, object_qnode_key=n01, virtual_relation_label=FET1, rel_edge_key=e00)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.001, remove_connected_nodes=t, qnode_keys=[n01])",
        "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n02)",
        "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:physically_interacts_with)",
        "expand(edge_key=e01, kp=infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n01, object_qnode_key=n02, virtual_relation_label=FET2, filter_type=cutoff, value=0.05)",
        "resultify()",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert 'biolink:has_fisher_exact_test_p_value_with' in edge_predicates_in_kg
    FET_edges = []
    FET_edge_labels = set()
    for edge in message.knowledge_graph.edges.values():
        relation_name = None
        add_edge = False
        for attribute in edge.attributes:
            if attribute.original_attribute_name == "virtual_relation_label":
                if attribute.value is not None and attribute.value.find("FET") != -1:
                    add_edge = True
                    FET_edge_labels.add(attribute.value)
                    relation_name = attribute.value
        if add_edge:
            FET_edges.append((edge, relation_name))
    assert len(FET_edges) > 0
    assert len(FET_edge_labels) == 2
    for edge_tuple in FET_edges:
        edge, relation_name = edge_tuple
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        edge_attributes_dict = {attr.original_attribute_name:attr.value for attr in edge.attributes}
        assert edge.attributes[0].original_attribute_name == 'fisher_exact_test_p-value'
        assert edge.attributes[0].attribute_type_id == 'EDAM:data_1669'
        #assert edge_attributes_dict['is_defined_by'] == 'ARAX'
        assert edge_attributes_dict['provided_by'] == 'infores:arax'
        if relation_name == 'FET1':
            assert 0 <= float(edge.attributes[0].value) < 0.001
        else:
            assert 0 <= float(edge.attributes[0].value) < 0.05
    FET_query_edges = {key:edge for key, edge in message.query_graph.edges.items() if key.find("FET") != -1}
    assert len(FET_query_edges) == 2
    query_node_keys = [key for key, node in message.query_graph.nodes.items()]
    assert len(query_node_keys) == 3
    for key, query_edge in FET_query_edges.items():
        assert hasattr(query_edge, 'predicates')
        assert 'biolink:has_fisher_exact_test_p_value_with' in query_edge.predicates
        assert key == query_edge.relation
        assert query_edge.subject in query_node_keys
        assert query_edge.object in query_node_keys


@pytest.mark.slow
def test_FET_ex2():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(ids=DOID:12889, key=n00, categories=biolink:Disease)",
        "add_qnode(categories=biolink:Protein, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(edge_key=e00, kp=infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n00, virtual_relation_label=FET, object_qnode_key=n01, rel_edge_key=e00, top_n=20)",
        "resultify()",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert 'biolink:has_fisher_exact_test_p_value_with' in edge_predicates_in_kg
    FET_edges = []
    FET_edge_labels = set()
    for edge in message.knowledge_graph.edges.values():
        relation_name = None
        add_edge = False
        for attribute in edge.attributes:
            if attribute.original_attribute_name == "virtual_relation_label":
                if attribute.value is not None and attribute.value.find("FET") != -1:
                    add_edge = True
                    FET_edge_labels.add(attribute.value)
                    relation_name = attribute.value
        if add_edge:
            FET_edges.append((edge, relation_name))
    assert len(FET_edges) >= 2
    assert len(FET_edge_labels) == 1
    for edge_tuple in FET_edges:
        edge, relation_name = edge_tuple
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        edge_attributes_dict = {attr.original_attribute_name:attr.value for attr in edge.attributes}
        assert edge.attributes[0].original_attribute_name == 'fisher_exact_test_p-value'
        assert edge.attributes[0].attribute_type_id == 'EDAM:data_1669'
        #assert edge_attributes_dict['is_defined_by'] == 'ARAX'
        assert edge_attributes_dict['provided_by'] == 'infores:arax'
    FET_query_edges = {key:edge for key, edge in message.query_graph.edges.items() if key.find("FET") != -1}
    assert len(FET_query_edges) == 1
    query_node_keys = [key for key, node in message.query_graph.nodes.items()]
    assert len(query_node_keys) == 2
    for key, query_edge in FET_query_edges.items():
        assert hasattr(query_edge, 'predicates')
        assert 'biolink:has_fisher_exact_test_p_value_with' in query_edge.predicates
        assert key == query_edge.relation
        assert query_edge.subject in query_node_keys
        assert query_edge.object in query_node_keys


@pytest.mark.slow
def test_paired_concept_frequency_virtual():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:1588, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=overlay_clinical_info, paired_concept_frequency=true, subject_qnode_key=n0, object_qnode_key=n1, virtual_relation_label=CP1)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    _virtual_tester(message, 'biolink:has_real_world_evidence_of_association_with', 'CP1', 'paired_concept_frequency', 'EDAM:data_0951', 2)


@pytest.mark.slow
def test_paired_concept_frequency_attribute():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:1588, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=overlay_clinical_info, COHD_method=paired_concept_frequency)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    _attribute_tester(message, 'paired_concept_frequency', 'EDAM:data_0951', 2)


@pytest.mark.slow
def test_observed_expected_ratio_virtual():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:1588, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=overlay_clinical_info,observed_expected_ratio=true, subject_qnode_key=n0, object_qnode_key=n1, virtual_relation_label=CP1)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    _virtual_tester(message, 'biolink:has_real_world_evidence_of_association_with', 'CP1', 'observed_expected_ratio', 'EDAM:data_0951', 2)


@pytest.mark.slow
def test_observed_expected_ratio_attribute():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:1588, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=overlay_clinical_info, COHD_method=observed_expected_ratio)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    _attribute_tester(message, 'observed_expected_ratio', 'EDAM:data_0951', 2)


@pytest.mark.slow
def test_chi_square_virtual():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:1588, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=overlay_clinical_info, chi_square=true, subject_qnode_key=n0, object_qnode_key=n1, virtual_relation_label=CP1)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    _virtual_tester(message, 'biolink:has_real_world_evidence_of_association_with', 'CP1', 'chi_square', 'EDAM:data_0951', 2)


@pytest.mark.slow
def test_chi_square_attribute():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:1588, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=overlay_clinical_info, COHD_method=chi_square)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    _attribute_tester(message, 'chi_square', 'EDAM:data_0951', 2)


# CM: I changed 'MONDO:0004992' to 'DOID:0080909' for a more specific disease
@pytest.mark.slow
def test_predict_drug_treats_disease_virtual():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(ids=DOID:0080909, key=n0, categories=biolink:Disease)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=predict_drug_treats_disease, subject_qnode_key=n1, object_qnode_key=n0, virtual_relation_label=P1)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    _virtual_tester(message, 'biolink:probably_treats', 'P1', 'probability_treats', 'EDAM:data_0951', 2)


# CM: I changed 'MONDO:0004992' to 'DOID:0080909' for a more specific disease
@pytest.mark.slow
def test_predict_drug_treats_disease_attribute():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(ids=DOID:0080909, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=predict_drug_treats_disease, threshold=0.7)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    _attribute_tester(message, 'probability_treats', 'EDAM:data_0951', 2)


# CM: I changed 'MONDO:0004992' to 'DOID:0080909' for a more specific disease
@pytest.mark.slow
def test_issue_832():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(ids=DOID:0080909, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=predict_drug_treats_disease, subject_qnode_key=n1, object_qnode_key=n0, virtual_relation_label=P1)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    _virtual_tester(message, 'biolink:probably_treats', 'P1', 'probability_treats', 'EDAM:data_0951', 2)


@pytest.mark.slow
def test_issue_832_non_drug():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(ids=UniProtKB:P62328, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=predict_drug_treats_disease, subject_qnode_key=n1, object_qnode_key=n0, virtual_relation_label=P1)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    # Make sure that no probability_treats were added
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert 'probability_treats' not in edge_predicates_in_kg


@pytest.mark.slow
def test_issue_840():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:1588, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=overlay_clinical_info, paired_concept_frequency=true, subject_qnode_key=n1, object_qnode_key=n0, virtual_relation_label=V1)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    _virtual_tester(message, 'biolink:has_real_world_evidence_of_association_with', 'V1', 'paired_concept_frequency', 'EDAM:data_0951', 2)

    # And for the non-virtual test
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:1588, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=overlay_clinical_info, paired_concept_frequency=true)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    _attribute_tester(message, 'paired_concept_frequency', 'EDAM:data_0951', 2)


@pytest.mark.slow
def test_issue_840_non_drug():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=UniProtKB:P62328, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=overlay_clinical_info, paired_concept_frequency=true, subject_qnode_key=n1, object_qnode_key=n0, virtual_relation_label=V1)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    # Make sure that no probability_treats were added
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert 'paired_concept_frequency' not in edge_predicates_in_kg

    # Now for the non-virtual test
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=UniProtKB:P62328, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(edge_key=e0, kp=infores:rtx-kg2)",
        "overlay(action=overlay_clinical_info, paired_concept_frequency=true)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    # Make sure that no probability_treats were added
    for edge in message.knowledge_graph.edges.values():
        for attribute in edge.attributes:
            assert attribute.original_attribute_name != 'paired_concept_frequency'


@pytest.mark.external
@pytest.mark.slow
def test_issue_892():
    query = {"operations": {"actions": [
        "add_qnode(ids=DOID:11830, categories=biolink:Disease, key=n00)",
        "add_qnode(categories=biolink:Gene, ids=[UniProtKB:P39060, UniProtKB:O43829, UniProtKB:P20849], is_set=true, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01)",
        "expand(kp=infores:biothings-explorer)",
        "overlay(action=predict_drug_treats_disease, subject_qnode_key=n02, object_qnode_key=n00, virtual_relation_label=P1, threshold=0.5)",
        "resultify(ignore_edge_direction=true)",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    _virtual_tester(message, 'biolink:probably_treats', 'P1', 'probability_treats', 'EDAM:data_0951', 10)


@pytest.mark.external
def test_overlay_exposures_data_virtual():
    query = {"operations": {"actions": [
        "add_qnode(name=CHEMBL.COMPOUND:CHEMBL635, key=n0)",
        "add_qnode(name=MESH:D052638, key=n1)",
        "expand(kp=infores:rtx-kg2)",
        "overlay(action=overlay_exposures_data, virtual_relation_label=E1, subject_qnode_key=n0, object_qnode_key=n1)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    print(response.show())
    _virtual_tester(message, 'biolink:has_icees_p-value_with', 'E1', 'icees_p-value', 'EDAM:data_1669', 1)


@pytest.mark.external
def test_overlay_exposures_data_attribute():
    query = {"operations": {"actions": [
        "add_qnode(name=MONDO:0012607, key=n0)",
        "add_qnode(name=MONDO:0010940, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(kp=infores:rtx-kg2)",
        "overlay(action=overlay_exposures_data)",
        "resultify()",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    print(response.show())
    _attribute_tester(message, 'icees_p-value', 'EDAM:data_1669', 1)


@pytest.mark.slow
def test_overlay_clinical_info_no_ids():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=acetaminophen, key=n0)",
        "add_qnode(name=Sotos syndrome, key=n1)",
        "expand(kp=infores:rtx-kg2)",
        "overlay(action=overlay_clinical_info,COHD_method=paired_concept_frequency,virtual_relation_label=C1,subject_qnode_key=n0,object_qnode_key=n1)",
        "overlay(action=overlay_clinical_info,COHD_method=observed_expected_ratio,virtual_relation_label=C2,subject_qnode_key=n0,object_qnode_key=n1)",
        "overlay(action=overlay_clinical_info,COHD_method=chi_square,virtual_relation_label=C3,subject_qnode_key=n0,object_qnode_key=n1)",
        "resultify()",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    _virtual_tester(message, 'biolink:has_real_world_evidence_of_association_with', 'C1', 'paired_concept_frequency', 'EDAM:data_0951', 1)
    _attribute_tester(message, 'paired_concept_frequency', 'EDAM:data_0951', 1)
    _virtual_tester(message, 'biolink:has_real_world_evidence_of_association_with', 'C2', 'observed_expected_ratio', 'EDAM:data_0951', 1)
    _attribute_tester(message, 'observed_expected_ratio', 'EDAM:data_0951', 1)
    _virtual_tester(message, 'biolink:has_real_world_evidence_of_association_with', 'C3', 'chi_square', 'EDAM:data_0951', 1)
    _attribute_tester(message, 'chi_square', 'EDAM:data_0951', 1)

@pytest.mark.slow
def test_missing_ngd_pmids():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(ids=UniProtKB:P52788, key=n0)",
        "add_qnode(categories=[biolink:Protein,biolink:Gene], key=n1, is_set=true)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "add_qnode(categories=[biolink:ChemicalEntity,biolink:Drug], key=n2)",
        "add_qedge(subject=n1, object=n2, key=e1)",
        "expand(kp=infores:rtx-kg2)",
        "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n0, object_qnode_key=n1)",
        "overlay(action=compute_ngd, virtual_relation_label=N2, subject_qnode_key=n1, object_qnode_key=n2)",
        "overlay(action=compute_ngd, virtual_relation_label=N3, subject_qnode_key=n0, object_qnode_key=n2)",
        "overlay(action=compute_jaccard, start_node_key=n0, intermediate_node_key=n1, end_node_key=n2, virtual_relation_label=J1)",
        "resultify()",
        "filter_results(action=limit_number_of_results, max_results=30)",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    ngd_publications = {}
    for edge_key, edge in message.knowledge_graph.edges.items():
        if edge.attributes is not None:
            for attribute in edge.attributes:
                if attribute.original_attribute_name == 'normalized_google_distance':
                    if edge_key not in ngd_publications:
                        ngd_publications[edge_key] = {}
                    ngd_publications[edge_key]['ngd'] = attribute.value
                elif attribute.original_attribute_name == 'publications':
                    if edge_key not in ngd_publications:
                        ngd_publications[edge_key] = {}
                    ngd_publications[edge_key]['pubs'] = attribute.value

    for edge_dict in ngd_publications.values():
        if 'ngd' in edge_dict and 'pubs' in edge_dict:
            if edge_dict['pubs'] == []:
                assert edge_dict['ngd'] == 'inf'

@pytest.mark.slow
def test_jaccard_not_above_1():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(key=N0,ids=chembl.compound:CHEMBL787)",
        "add_qnode(key=N1,categories=biolink:Protein)",
        "add_qedge(key=E0,subject=N0,object=N1,predicates=biolink:physically_interacts_with)",
        "add_qnode(key=N2,categories=biolink:ChemicalEntity)",
        "add_qedge(key=E2,subject=N1,object=N2)",
        "expand(kp=infores:rtx-kg2)",
        "overlay(action=compute_ngd,default_value=inf,virtual_relation_label=V1,subject_qnode_key=N0,object_qnode_key=N1)",
        "overlay(action=compute_ngd,default_value=inf,virtual_relation_label=V2,subject_qnode_key=N1,object_qnode_key=N2)",
        "overlay(action=compute_ngd,default_value=inf,virtual_relation_label=V3,subject_qnode_key=N0,object_qnode_key=N2)",
        "overlay(action=compute_jaccard,start_node_key=N0,intermediate_node_key=N1,end_node_key=N2,virtual_relation_label=VJ)",
        "resultify()",
        #"filter_results(action=limit_number_of_results,max_results=100,prune_kg=true)",
        "return(message=true, store=false)",
        ]}}
    [response, message] = _do_arax_query(query)
    print(response.show())
    assert response.status == 'OK'
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert 'biolink:has_jaccard_index_with' in edge_predicates_in_kg
    jaccard_edges = []
    for edge in message.knowledge_graph.edges.values():
        add_edge = False
        for attribute in edge.attributes:
            if attribute.original_attribute_name == "virtual_relation_label":
                if attribute.value == "VJ":
                    add_edge = True
        if add_edge:
            jaccard_edges.append(edge)
    assert len(jaccard_edges) > 0
    for edge in jaccard_edges:
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        assert edge.attributes[0].original_attribute_name == 'jaccard_index'
        assert edge.attributes[0].value >= 0
        assert edge.attributes[0].value <= 1

@pytest.mark.slow
def test_ngd_sqlite_syntax_error():
    query = {"message":{"query_graph":{
          "edges": {
            "e00": {
              "object": "n01",
              "subject": "n00"
            }
          },
          "nodes": {
            "n00": {
              "ids": [
                "CHEBI:16680"
              ],
              "is_set": False
            },
            "n01": {
              "categories": [
                "biolink:Pathway",
                "biolink:BiologicalProcess",
                "biolink:MolecularActivity",
                "biolink:CellularComponent",
                "biolink:InformationContentEntity",
                "biolink:NamedThing"
              ],
              "is_set": False
            }
          }
        }
    }}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'


if __name__ == "__main__":
    pytest.main(['-v'])


# code/ARAX/test/test_ARAX_query.py

#!/usr/bin/env python3

import sys
import os
import pytest

import copy
import json
import ast

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
from ARAX_query import ARAXQuery


#def test_query_by_canned_query_Q0():
#    query = { 'message': { 'query_type_id': 'Q0', 'terms': { 'term': 'lovastatin' } } }
#    araxq = ARAXQuery()
#    result = araxq.query(query)
#    print(result.show())
#    assert result.status == 'OK'
#    message = araxq.message
#    assert message.n_results == 1
#    assert message.type == 'translator_reasoner_message'
#    assert message.schema_version == '0.9.2'                    # FIXME

def test_query_by_query_graph_2():
    query = { "message": { "query_graph": { "edges": {
                "qg2": { "subject": "qg1", "object": "qg0", "predicates": ["biolink:physically_interacts_with"] }
            },
            "nodes": {
                "qg0": { "name": "acetaminophen", "ids": ["CHEMBL.COMPOUND:CHEMBL112"], "categories": ["biolink:ChemicalEntity"] },
                "qg1": { "name": None, "ids": None, "categories": ["biolink:Protein"] }
            } } } }
    araxq = ARAXQuery()
    araxq.query(query)
    response = araxq.response
    print(response.show())

    assert response.status == 'OK'
    message = response.envelope.message
    assert len(message.results) >= 32
    assert response.envelope.schema_version == '1.3.0'


if __name__ == "__main__": pytest.main(['-v'])


# code/ARAX/test/test_ARAX_resultify.py

#!/usr/bin/env python3
# Usage:  python3 ARAX_resultify_testcases.py
#         python3 ARAX_resultify_testcases.py test_issue692

import os
import sys
import pytest

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
from ARAX_response import ARAXResponse
from ARAX_messenger import ARAXMessenger
from typing import List, Union, Dict, Tuple, Set, Iterable

import ARAX_resultify
from ARAX_resultify import ARAXResultify
from ARAX_query import ARAXQuery

# is there a better way to import openapi_server?  Following SO posting 16981921
PACKAGE_PARENT = '../../UI/OpenAPI/python-flask-server'
sys.path.append(os.path.normpath(os.path.join(os.getcwd(), PACKAGE_PARENT)))
from openapi_server.models.edge import Edge
from openapi_server.models.node import Node
from openapi_server.models.q_edge import QEdge
from openapi_server.models.q_node import QNode
from openapi_server.models.query_graph import QueryGraph
from openapi_server.models.knowledge_graph import KnowledgeGraph
from openapi_server.models.result import Result
from openapi_server.models.message import Message


def _slim_kg(kg: KnowledgeGraph) -> KnowledgeGraph:
    slimmed_nodes = {node_key: Node(categories=node.categories,
                                    name=node.name,
                                    qnode_keys=node.qnode_keys) for node_key, node in kg.nodes.items()}
    slimmed_edges = {edge_key: Edge(subject=edge.subject,
                                    object=edge.object,
                                    predicate=edge.predicate,
                                    qedge_keys=edge.qedge_keys) for edge_key, edge in kg.edges.items()}
    return KnowledgeGraph(nodes=slimmed_nodes, edges=slimmed_edges)


def _create_nodes(kg_node_info: Iterable[Dict[str, any]]) -> Dict[str, Node]:
    nodes_dict = dict()
    for kg_node in kg_node_info:
        node = Node(categories=kg_node.get("categories"),
                    name=kg_node.get("name"))
        node.qnode_keys = kg_node["qnode_keys"]
        nodes_dict[kg_node["node_key"]] = node
    return nodes_dict


def _create_edges(kg_edge_info: Iterable[Dict[str, any]]) -> Dict[str, Edge]:
    edges_dict = dict()
    for kg_edge in kg_edge_info:
        edge = Edge(subject=kg_edge["subject"],
                    object=kg_edge["object"],
                    predicate=kg_edge.get("predicate"))
        edge.qedge_keys = kg_edge["qedge_keys"]
        edges_dict[kg_edge["edge_key"]] = edge
    return edges_dict


def _create_qnodes(qg_node_info: Iterable[Dict[str, any]]) -> Dict[str, QNode]:
    return {qnode_info["node_key"]: QNode(categories=qnode_info['categories'],
                                          is_set=qnode_info['is_set']) for qnode_info in qg_node_info}


def _create_qedges(qg_edge_info: Iterable[Dict[str, any]]) -> Dict[str, QEdge]:
    return {qedge_info["edge_key"]: QEdge(subject=qedge_info['subject'],
                                          object=qedge_info['object']) for qedge_info in qg_edge_info}


def _print_results_for_debug(message: Message):
    print()
    qg = message.query_graph
    kg = message.knowledge_graph
    for result in message.results:
        print(result.essence)
        for qnode_key, node_bindings_list in result.node_bindings.items():
            qnode = qg.nodes[qnode_key]
            print(f"  qnode {qnode_key}{f' (option group {qnode.option_group_id})' if qnode.option_group_id else ''}:")
            for node_binding in node_bindings_list:
                print(f"    {node_binding.id} {kg.nodes[node_binding.id].name}")
        for qedge_key, edge_bindings_list in result.edge_bindings.items():
            qedge = qg.edges[qedge_key]
            print(f"  qedge {qedge_key}{f' (option group {qedge.option_group_id})' if qedge.option_group_id else ''}:")
            for edge_binding in edge_bindings_list:
                print(f"    {edge_binding.id}")
    # Display the query graph
    import graphviz
    dot = graphviz.Digraph(comment='QG')
    for qnode_key, qnode in qg.nodes.items():
        node_id_line = f"{qnode_key}{f' (group {qnode.option_group_id})' if qnode.option_group_id else ''}"
        if qnode.ids:
            node_details_line = ", ".join(qnode.ids)
        elif qnode.categories:
            node_details_line = ", ".join(qnode.categories)
        else:
            node_details_line = ""
        dot.node(qnode_key, f"{node_id_line}\n{node_details_line}")
    for qedge_key, qedge in qg.edges.items():
        dot.edge(qedge.subject,
                 qedge.object,
                 label=f"{qedge_key}{f' (NOT)' if qedge.exclude else ''}{f' (group {qedge.option_group_id})' if qedge.option_group_id else ''}\n{', '.join(qedge.predicates) if qedge.predicates else ''}")
    dot.render("qg.gv", view=True)


def _get_result_node_keys_by_qg_key(result: Result) -> Dict[str, Set[str]]:
    return {qnode_key: {node_binding.id for node_binding in result.node_bindings[qnode_key]} for qnode_key in result.node_bindings}


def _get_result_edge_keys_by_qg_key(result: Result) -> Dict[str, Set[str]]:
    return {qedge_key: {edge_binding.id for edge_binding in result.edge_bindings[qedge_key]} for qedge_key in result.edge_bindings}


def _do_arax_query(actions_list: List[str], debug=False) -> Tuple[ARAXResponse, Message]:
    query = {"operations": {"actions": actions_list}}
    araxq = ARAXQuery()
    response = araxq.query(query)
    message = araxq.message
    if debug:
        _print_results_for_debug(message)
        print(response.show(level=response.DEBUG))
    elif response.status != 'OK':
        print(response.show(level=response.DEBUG))
    return response, message


def _run_resultify_directly(query_graph: QueryGraph,
                            knowledge_graph: KnowledgeGraph,
                            ignore_edge_direction=True,
                            debug=False) -> Tuple[ARAXResponse, Message]:
    response = ARAXResponse()
    messenger = ARAXMessenger()
    messenger.create_envelope(response)
    from actions_parser import ActionsParser
    actions_parser = ActionsParser()
    actions_list = [f"resultify(ignore_edge_direction={ignore_edge_direction})"]
    result = actions_parser.parse(actions_list)
    response.merge(result)
    actions = result.data['actions']
    assert result.status == 'OK'
    resultifier = ARAXResultify()
    message_original = Message(query_graph=query_graph,
                               knowledge_graph=knowledge_graph,
                               results=[])
    message = ARAXMessenger().from_dict(message_original.to_dict())
    # qnode_keys/qedge_keys are lost when grabbing message from_dict() - so we add them back
    for node_key, node in message_original.knowledge_graph.nodes.items():
        message.knowledge_graph.nodes[node_key].qnode_keys = node.qnode_keys
    for edge_key, edge in message_original.knowledge_graph.edges.items():
        message.knowledge_graph.edges[edge_key].qedge_keys = edge.qedge_keys
    response.envelope.message = message
    parameters = actions[0]['parameters']
    parameters['debug'] = 'true'
    resultifier.apply(response, parameters)
    if response.status != 'OK':
        if debug:
            _print_results_for_debug(message)
        print(response.show(level=response.DEBUG))
    return response, message


def _convert_shorthand_to_qg(shorthand_qnodes: Dict[str, str], shorthand_qedges: Dict[str, str]) -> QueryGraph:
    return QueryGraph(nodes={qnode_key: QNode(is_set=bool(is_set)) for qnode_key, is_set in shorthand_qnodes.items()},
                      edges={qedge_key: QEdge(subject=qnodes.split("--")[0],
                                              object=qnodes.split("--")[1]) for qedge_key, qnodes in shorthand_qedges.items()})


def _convert_shorthand_to_kg(shorthand_nodes: Dict[str, List[str]], shorthand_edges: Dict[str, List[str]]) -> KnowledgeGraph:
    nodes_dict = dict()
    for qnode_key, nodes_list in shorthand_nodes.items():
        for node_key in nodes_list:
            node = nodes_dict.get(node_key, Node())
            if not hasattr(node, "qnode_keys"):
                node.qnode_keys = []
            node.qnode_keys.append(qnode_key)
            nodes_dict[node_key] = node
    edges_dict = dict()
    for qedge_key, edges_list in shorthand_edges.items():
        for edge_key in edges_list:
            source_node_key = edge_key.split("--")[0]
            target_node_key = edge_key.split("--")[1]
            edge = edges_dict.get(edge_key, Edge(subject=source_node_key, object=target_node_key))
            if not hasattr(edge, "qedge_keys"):
                edge.qedge_keys = []
            edge.qedge_keys.append(qedge_key)
            edges_dict[f"{qedge_key}:{edge_key}"] = edge
    return KnowledgeGraph(nodes=nodes_dict, edges=edges_dict)


def _get_kg_edge_keys_using_node(node_key: str, kg: KnowledgeGraph) -> Set[str]:
    return {edge_key for edge_key, edge in kg.edges.items() if node_key in {edge.subject, edge.object}}


def test01():
    kg_node_info = ({'node_key': 'UniProtKB:12345',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'UniProtKB:23456',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'qnode_keys': ['DOID:12345']},
                    {'node_key': 'HP:56789',
                     'categories': 'phenotypic_feature',
                     'qnode_keys': ['n02']},
                    {'node_key': 'HP:67890',
                     'categories': 'phenotypic_feature',
                     'qnode_keys': ['n02']},
                    {'node_key': 'HP:34567',
                     'categories': 'phenotypic_feature',
                     'qnode_keys': ['n02']})

    kg_edge_info = ({'edge_key': 'ke01',
                     'subject': 'UniProtKB:12345',
                     'object': 'DOID:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke02',
                     'subject': 'UniProtKB:23456',
                     'object': 'DOID:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke03',
                     'subject': 'DOID:12345',
                     'object': 'HP:56789',
                     'qedge_keys': ['qe02']},
                    {'edge_key': 'ke04',
                     'subject': 'DOID:12345',
                     'object': 'HP:67890',
                     'qedge_keys': ['qe02']},
                    {'edge_key': 'ke05',
                     'subject': 'DOID:12345',
                     'object': 'HP:34567',
                     'qedge_keys': ['qe02']})

    kg_nodes = _create_nodes(kg_node_info)
    kg_edges = _create_edges(kg_edge_info)

    knowledge_graph = KnowledgeGraph(kg_nodes, kg_edges)

    qg_node_info = ({'node_key': 'n01',
                     'categories': 'protein',
                     'is_set': False},
                    {'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'is_set': False},
                    {'node_key': 'n02',
                     'categories': 'phenotypic_feature',
                     'is_set': True})

    qg_edge_info = ({'edge_key': 'qe01',
                     'subject': 'n01',
                     'object': 'DOID:12345'},
                    {'edge_key': 'qe02',
                     'subject': 'DOID:12345',
                     'object': 'n02'})

    qg_nodes = _create_qnodes(qg_node_info)
    qg_edges = _create_qedges(qg_edge_info)
    query_graph = QueryGraph(qg_nodes, qg_edges)

    results_list = ARAX_resultify._get_results_for_kg_by_qg(knowledge_graph,
                                                            query_graph)

    assert len(results_list) == 2


def test02():
    kg_node_info = ({'node_key': 'UniProtKB:12345',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'UniProtKB:23456',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'qnode_keys': ['DOID:12345']},
                    {'node_key': 'HP:56789',
                     'categories': 'phenotypic_feature',
                     'qnode_keys': ['n02']},
                    {'node_key': 'HP:67890',
                     'categories': 'phenotypic_feature',
                     'qnode_keys': ['n02']},
                    {'node_key': 'HP:34567',
                     'categories': 'phenotypic_feature',
                     'qnode_keys': ['n02']})

    kg_edge_info = ({'edge_key': 'ke01',
                     'subject': 'UniProtKB:12345',
                     'object': 'DOID:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke02',
                     'subject': 'UniProtKB:23456',
                     'object': 'DOID:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke03',
                     'subject': 'DOID:12345',
                     'object': 'HP:56789',
                     'qedge_keys': ['qe02']},
                    {'edge_key': 'ke04',
                     'subject': 'DOID:12345',
                     'object': 'HP:67890',
                     'qedge_keys': ['qe02']},
                    {'edge_key': 'ke05',
                     'subject': 'DOID:12345',
                     'object': 'HP:34567',
                     'qedge_keys': ['qe02']})

    kg_nodes = _create_nodes(kg_node_info)
    kg_edges = _create_edges(kg_edge_info)

    knowledge_graph = KnowledgeGraph(kg_nodes, kg_edges)

    qg_node_info = ({'node_key': 'n01',
                     'categories': 'protein',
                     'is_set': None},
                    {'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'is_set': False},
                    {'node_key': 'n02',
                     'categories': 'phenotypic_feature',
                     'is_set': True})

    qg_edge_info = ({'edge_key': 'qe01',
                     'subject': 'n01',
                     'object': 'DOID:12345'},
                    {'edge_key': 'qe02',
                     'subject': 'DOID:12345',
                     'object': 'n02'})

    qg_nodes = _create_qnodes(qg_node_info)
    qg_edges = _create_qedges(qg_edge_info)
    query_graph = QueryGraph(qg_nodes, qg_edges)

    results_list = ARAX_resultify._get_results_for_kg_by_qg(knowledge_graph,
                                                            query_graph)
    assert len(results_list) == 2


def test03():
    kg_node_info = ({'node_key': 'UniProtKB:12345',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'UniProtKB:23456',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'qnode_keys': ['DOID:12345']},
                    {'node_key': 'HP:56789',
                     'categories': 'phenotypic_feature',
                     'qnode_keys': ['n02']},
                    {'node_key': 'HP:67890',
                     'categories': 'phenotypic_feature',
                     'qnode_keys': ['n02']},
                    {'node_key': 'HP:34567',
                     'categories': 'phenotypic_feature',
                     'qnode_keys': ['n02']})

    kg_edge_info = ({'edge_key': 'ke01',
                     'subject': 'DOID:12345',
                     'object': 'UniProtKB:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke02',
                     'subject': 'UniProtKB:23456',
                     'object': 'DOID:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke03',
                     'subject': 'DOID:12345',
                     'object': 'HP:56789',
                     'qedge_keys': ['qe02']},
                    {'edge_key': 'ke04',
                     'subject': 'DOID:12345',
                     'object': 'HP:67890',
                     'qedge_keys': ['qe02']},
                    {'edge_key': 'ke05',
                     'subject': 'DOID:12345',
                     'object': 'HP:34567',
                     'qedge_keys': ['qe02']})

    kg_nodes = _create_nodes(kg_node_info)
    kg_edges = _create_edges(kg_edge_info)

    knowledge_graph = KnowledgeGraph(kg_nodes, kg_edges)

    qg_node_info = ({'node_key': 'n01',
                     'categories': 'protein',
                     'is_set': None},
                    {'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'is_set': False},
                    {'node_key': 'n02',
                     'categories': 'phenotypic_feature',
                     'is_set': True})

    qg_edge_info = ({'edge_key': 'qe01',
                     'subject': 'n01',
                     'object': 'DOID:12345'},
                    {'edge_key': 'qe02',
                     'subject': 'DOID:12345',
                     'object': 'n02'})

    qg_nodes = _create_qnodes(qg_node_info)
    qg_edges = _create_qedges(qg_edge_info)
    query_graph = QueryGraph(qg_nodes, qg_edges)

    results_list = ARAX_resultify._get_results_for_kg_by_qg(knowledge_graph,
                                                            query_graph,
                                                            ignore_edge_direction=True)
    assert len(results_list) == 2


def test04():
    kg_node_info = ({'node_key': 'UniProtKB:12345',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'UniProtKB:23456',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'qnode_keys': ['DOID:12345']},
                    {'node_key': 'UniProtKB:56789',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'ChEMBL.COMPOUND:12345',
                     'categories': 'chemical_substance',
                     'qnode_keys': ['n02']},
                    {'node_key': 'ChEMBL.COMPOUND:23456',
                     'categories': 'chemical_substance',
                     'qnode_keys': ['n02']})

    kg_edge_info = ({'edge_key': 'ke01',
                     'subject': 'ChEMBL.COMPOUND:12345',
                     'object': 'UniProtKB:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke02',
                     'subject': 'ChEMBL.COMPOUND:12345',
                     'object': 'UniProtKB:23456',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke03',
                     'subject': 'ChEMBL.COMPOUND:23456',
                     'object': 'UniProtKB:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke04',
                     'subject': 'ChEMBL.COMPOUND:23456',
                     'object': 'UniProtKB:23456',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke05',
                     'subject': 'DOID:12345',
                     'object': 'UniProtKB:12345',
                     'qedge_keys': ['qe02']},
                    {'edge_key': 'ke06',
                     'subject': 'DOID:12345',
                     'object': 'UniProtKB:23456',
                     'qedge_keys': ['qe02']})

    kg_nodes = _create_nodes(kg_node_info)
    kg_edges = _create_edges(kg_edge_info)

    knowledge_graph = KnowledgeGraph(kg_nodes, kg_edges)

    qg_node_info = ({'node_key': 'n01',
                     'categories': 'protein',
                     'is_set': True},
                    {'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'is_set': False},
                    {'node_key': 'n02',
                     'categories': 'chemical_substance',
                     'is_set': False})

    qg_edge_info = ({'edge_key': 'qe01',
                     'subject': 'n02',
                     'object': 'n01'},
                    {'edge_key': 'qe02',
                     'subject': 'DOID:12345',
                     'object': 'n01'})

    qg_nodes = _create_qnodes(qg_node_info)
    qg_edges = _create_qedges(qg_edge_info)
    query_graph = QueryGraph(qg_nodes, qg_edges)

    results_list = ARAX_resultify._get_results_for_kg_by_qg(knowledge_graph,
                                                            query_graph,
                                                            ignore_edge_direction=True)
    assert len(results_list) == 2


def test05():
    kg_node_info = ({'node_key': 'UniProtKB:12345',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'UniProtKB:23456',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'qnode_keys': ['DOID:12345']},
                    {'node_key': 'UniProtKB:56789',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'ChEMBL.COMPOUND:12345',
                     'categories': 'chemical_substance',
                     'qnode_keys': ['n02']},
                    {'node_key': 'ChEMBL.COMPOUND:23456',
                     'categories': 'chemical_substance',
                     'qnode_keys': ['n02']})

    kg_edge_info = ({'edge_key': 'ke01',
                     'subject': 'ChEMBL.COMPOUND:12345',
                     'object': 'UniProtKB:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke02',
                     'subject': 'ChEMBL.COMPOUND:12345',
                     'object': 'UniProtKB:23456',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke03',
                     'subject': 'ChEMBL.COMPOUND:23456',
                     'object': 'UniProtKB:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke04',
                     'subject': 'ChEMBL.COMPOUND:23456',
                     'object': 'UniProtKB:23456',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke05',
                     'subject': 'DOID:12345',
                     'object': 'UniProtKB:12345',
                     'qedge_keys': ['qe02']},
                    {'edge_key': 'ke06',
                     'subject': 'DOID:12345',
                     'object': 'UniProtKB:23456',
                     'qedge_keys': ['qe02']})

    kg_nodes = _create_nodes(kg_node_info)
    kg_edges = _create_edges(kg_edge_info)
    knowledge_graph = KnowledgeGraph(kg_nodes, kg_edges)

    qg_node_info = ({'node_key': 'n01',
                     'categories': 'protein',
                     'is_set': True},
                    {'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'is_set': False},
                    {'node_key': 'n02',
                     'categories': 'chemical_substance',
                     'is_set': False})

    qg_edge_info = ({'edge_key': 'qe01',
                     'subject': 'n02',
                     'object': 'n01'},
                    {'edge_key': 'qe02',
                     'subject': 'DOID:12345',
                     'object': 'n01'})

    qg_nodes = _create_qnodes(qg_node_info)
    qg_edges = _create_qedges(qg_edge_info)
    query_graph = QueryGraph(qg_nodes, qg_edges)

    response, message = _run_resultify_directly(query_graph, knowledge_graph, ignore_edge_direction=True)
    assert response.status == 'OK'
    assert len(message.results) == 2


def test07():
    kg_node_info = ({'node_key': 'UniProtKB:12345',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'UniProtKB:23456',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'qnode_keys': ['DOID:12345']},
                    {'node_key': 'UniProtKB:56789',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'ChEMBL.COMPOUND:12345',
                     'categories': 'chemical_substance',
                     'qnode_keys': ['n02']},
                    {'node_key': 'ChEMBL.COMPOUND:23456',
                     'categories': 'chemical_substance',
                     'qnode_keys': ['n02']})

    kg_edge_info = ({'edge_key': 'ke01',
                     'subject': 'ChEMBL.COMPOUND:12345',
                     'object': 'UniProtKB:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke02',
                     'subject': 'ChEMBL.COMPOUND:12345',
                     'object': 'UniProtKB:23456',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke03',
                     'subject': 'ChEMBL.COMPOUND:23456',
                     'object': 'UniProtKB:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke04',
                     'subject': 'ChEMBL.COMPOUND:23456',
                     'object': 'UniProtKB:23456',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke05',
                     'subject': 'DOID:12345',
                     'object': 'UniProtKB:12345',
                     'qedge_keys': ['qe02']},
                    {'edge_key': 'ke06',
                     'subject': 'DOID:12345',
                     'object': 'UniProtKB:23456',
                     'qedge_keys': ['qe02']})

    kg_nodes = _create_nodes(kg_node_info)
    kg_edges = _create_edges(kg_edge_info)

    knowledge_graph = KnowledgeGraph(kg_nodes, kg_edges)

    qg_node_info = ({'node_key': 'n01',
                     'categories': 'protein',
                     'is_set': True},
                    {'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'is_set': False},
                    {'node_key': 'n02',
                     'categories': 'chemical_substance',
                     'is_set': False})

    qg_edge_info = ({'edge_key': 'qe01',
                     'subject': 'n02',
                     'object': 'n01'},
                    {'edge_key': 'qe02',
                     'subject': 'DOID:12345',
                     'object': 'n01'})

    qg_nodes = _create_qnodes(qg_node_info)
    qg_edges = _create_qedges(qg_edge_info)
    query_graph = QueryGraph(qg_nodes, qg_edges)

    response, message = _run_resultify_directly(query_graph, knowledge_graph, ignore_edge_direction=True)
    assert len(message.results) == 2
    assert response.status == 'OK'


def test08():
    shorthand_qnodes = {"n00": "",
                        "n01": ""}
    shorthand_qedges = {"e00": "n00--n01"}
    query_graph = _convert_shorthand_to_qg(shorthand_qnodes, shorthand_qedges)
    shorthand_kg_nodes = {"n00": ["DOID:731"],
                          "n01": ["HP:01", "HP:02", "HP:03", "HP:04"]}
    shorthand_kg_edges = {"e00": ["DOID:731--HP:01", "DOID:731--HP:02", "DOID:731--HP:03", "DOID:731--HP:04"]}
    knowledge_graph = _convert_shorthand_to_kg(shorthand_kg_nodes, shorthand_kg_edges)
    response, message = _run_resultify_directly(query_graph, knowledge_graph)
    assert response.status == 'OK'
    n01_nodes = {node_key for node_key, node in message.knowledge_graph.nodes.items() if "n01" in node.qnode_keys}
    assert message.results and len(message.results) == len(n01_nodes)


@pytest.mark.slow
def test09():
    actions = [
        "add_qnode(name=DOID:731, key=n00, categories=biolink:Disease, is_set=false)",
        "add_qnode(categories=biolink:PhenotypicFeature, is_set=false, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(edge_key=e00, kp=infores:rtx-kg2)",
        "resultify(ignore_edge_direction=true, debug=true)",
        "filter_results(action=limit_number_of_results, max_results=100)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert len(message.results) == 100


def test10():
    resultifier = ARAXResultify()
    desc = resultifier.describe_me()
    assert "description" in desc[0]
    assert "ignore_edge_direction" in desc[0]["parameters"]


@pytest.mark.slow
def test_example1():
    actions = [
        "add_qnode(key=qg0, ids=CHEMBL.COMPOUND:CHEMBL112)",
        "add_qnode(key=qg1, categories=biolink:Protein)",
        "add_qedge(subject=qg1, object=qg0, key=qe0)",
        "expand(edge_key=qe0, kp=infores:rtx-kg2)",
        "resultify(ignore_edge_direction=true, debug=true)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    qg1_nodes = {node_key for node_key, node in message.knowledge_graph.nodes.items() if "qg1" in node.qnode_keys}
    assert message.results and len(message.results) == len(qg1_nodes)
    assert message.results[0].essence is not None


def test_bfs():
    qg_node_info = ({'node_key': 'n01',
                     'categories': 'protein',
                     'is_set': None},
                    {'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'is_set': False},
                    {'node_key': 'n02',
                     'categories': 'phenotypic_feature',
                     'is_set': True})

    qg_edge_info = ({'edge_key': 'qe01',
                     'subject': 'n01',
                     'object': 'DOID:12345'},
                    {'edge_key': 'qe02',
                     'subject': 'DOID:12345',
                     'object': 'n02'})

    qg_nodes = _create_qnodes(qg_node_info)
    qg_edges = _create_qedges(qg_edge_info)
    qg = QueryGraph(qg_nodes, qg_edges)
    adj_map = ARAX_resultify._make_adj_maps(qg, directed=False, droploops=True)['both']
    bfs_dists = ARAX_resultify._bfs_dists(adj_map, 'n01')
    assert bfs_dists == {'n01': 0, 'DOID:12345': 1, 'n02': 2}
    bfs_dists = ARAX_resultify._bfs_dists(adj_map, 'DOID:12345')
    assert bfs_dists == {'n01': 1, 'DOID:12345': 0, 'n02': 1}


def test_bfs_in_essence_code():
    kg_node_info = ({'node_key': 'DOID:12345',
                     'categories': 'disease',
                     'qnode_keys': ['n00']},
                    {'node_key': 'UniProtKB:12345',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'UniProtKB:23456',
                     'categories': 'protein',
                     'qnode_keys': ['n01']},
                    {'node_key': 'FOO:12345',
                     'categories': 'gene',
                     'qnode_keys': ['n02']},
                    {'node_key': 'HP:56789',
                     'categories': 'phenotypic_feature',
                     'qnode_keys': ['n03']})

    kg_edge_info = ({'edge_key': 'ke01',
                     'object': 'UniProtKB:12345',
                     'subject': 'DOID:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke02',
                     'object': 'UniProtKB:23456',
                     'subject': 'DOID:12345',
                     'qedge_keys': ['qe01']},
                    {'edge_key': 'ke03',
                     'subject': 'UniProtKB:12345',
                     'object': 'FOO:12345',
                     'qedge_keys': ['qe02']},
                    {'edge_key': 'ke04',
                     'subject': 'UniProtKB:23456',
                     'object': 'FOO:12345',
                     'qedge_keys': ['qe02']},
                    {'edge_key': 'ke05',
                     'subject': 'FOO:12345',
                     'object': 'HP:56789',
                     'qedge_keys': ['qe03']})

    kg_nodes = _create_nodes(kg_node_info)
    kg_edges = _create_edges(kg_edge_info)

    knowledge_graph = KnowledgeGraph(kg_nodes, kg_edges)

    qg_node_info = ({'node_key': 'n00',  # DOID:12345
                     'categories': 'disease',
                     'is_set': False},
                    {'node_key': 'n01',
                     'categories': 'protein',
                     'is_set': False},
                    {'node_key': 'n02',
                     'categories': 'gene',
                     'is_set': False},
                    {'node_key': 'n03',  # HP:56789
                     'categories': 'phenotypic_feature',
                     'is_set': False})

    qg_edge_info = ({'edge_key': 'qe01',
                     'subject': 'n00',
                     'object': 'n01'},
                    {'edge_key': 'qe02',
                     'subject': 'n01',
                     'object': 'n02'},
                    {'edge_key': 'qe03',
                     'subject': 'n02',
                     'object': 'n03'})

    qg_nodes = _create_qnodes(qg_node_info)
    qg_edges = _create_qedges(qg_edge_info)
    query_graph = QueryGraph(qg_nodes, qg_edges)

    results_list = ARAX_resultify._get_results_for_kg_by_qg(knowledge_graph,
                                                            query_graph)
    assert len(results_list) == 2
    assert results_list[0].essence is not None


@pytest.mark.slow
def test_issue680():
    actions = [
        "add_qnode(ids=DOID:14330, key=n00, categories=biolink:Disease)",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:causes)",
        "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:physically_interacts_with)",
        "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=jaccard_index, direction=below, threshold=.2, remove_connected_nodes=t, qnode_keys=[n02])",
        "resultify(ignore_edge_direction=true, debug=true)",
        "return(message=true, store=false)",
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert message.results[0].essence is not None
    kg = message.knowledge_graph
    for result in message.results:
        result_nodes_by_qg_id = _get_result_node_keys_by_qg_key(result)
        result_edges_by_qg_id = _get_result_edge_keys_by_qg_key(result)
        # Make sure all intermediate nodes are connected to at least one (real, not virtual) edge on BOTH sides
        for n01_node_key in result_nodes_by_qg_id['n01']:
            assert any(edge_key for edge_key in result_edges_by_qg_id['e00'] if
                       kg.edges[edge_key].subject == n01_node_key or kg.edges[edge_key].object == n01_node_key)
            assert any(edge_key for edge_key in result_edges_by_qg_id['e01'] if
                       kg.edges[edge_key].subject == n01_node_key or kg.edges[edge_key].object == n01_node_key)
        # Make sure all edges' nodes actually exist in this result (includes virtual and real edges)
        for qedge_key, edge_keys in result_edges_by_qg_id.items():
            qedge = message.query_graph.edges[qedge_key]
            for edge_key in edge_keys:
                edge = kg.edges[edge_key]
                assert (edge.subject in result_nodes_by_qg_id[qedge.subject] and edge.object in
                        result_nodes_by_qg_id[qedge.object]) or \
                       (edge.object in result_nodes_by_qg_id[qedge.subject] and edge.subject in
                        result_nodes_by_qg_id[qedge.object])


def test_issue686a():
    # Tests that an error is thrown when an invalid parameter is passed to resultify
    actions = [
        'add_qnode(key=qg0, ids=CHEMBL.COMPOUND:CHEMBL112)',
        'expand(kp=infores:rtx-kg2)',
        'resultify(ignore_edge_direction=true, INVALID_PARAMETER_NAME=true)',
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert 'INVALID_PARAMETER_NAME' in response.show()


def test_issue686b():
    # Tests that resultify can be called with no parameters passed in
    actions = [
        'add_qnode(key=qg0, ids=CHEMBL.COMPOUND:CHEMBL112)',
        'expand(kp=infores:rtx-kg2)',
        'resultify()',
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'


def test_issue686c():
    # Tests that setting ignore_edge_direction to an invalid value results in an error
    actions = [
        'add_qnode(key=qg0, ids=CHEMBL.COMPOUND:CHEMBL112)',
        'expand(kp=infores:rtx-kg2)',
        'resultify(ignore_edge_direction=foo)',
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status != 'OK' and 'foo' in response.show()


def test_issue687():
    # Tests that ignore_edge_direction need not be specified
    actions = [
        'add_qnode(key=qg0, ids=CHEMBL.COMPOUND:CHEMBL112)',
        'expand(kp=infores:rtx-kg2)',
        'resultify(debug=true)',
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert message.results and len(message.results) == len(message.knowledge_graph.nodes)


def test_issue727():
    # Check resultify ignores edge direction appropriately
    shorthand_qnodes = {"n00": "",
                        "n01": ""}
    shorthand_qedges = {"e00": "n00--n01"}
    query_graph = _convert_shorthand_to_qg(shorthand_qnodes, shorthand_qedges)
    shorthand_kg_nodes = {"n00": ["DOID:111"],
                          "n01": ["PR:01", "PR:02"]}
    shorthand_kg_edges = {"e00": ["PR:01--DOID:111", "PR:02--DOID:111"]}  # Edges are reverse direction of QG
    knowledge_graph = _convert_shorthand_to_kg(shorthand_kg_nodes, shorthand_kg_edges)
    response, message = _run_resultify_directly(query_graph, knowledge_graph)
    assert response.status == 'OK'
    assert len(message.results) == 2


def test_issue731():
    # Return no results if QG is unfulfilled
    shorthand_qnodes = {"n0": "",
                        "n1": "is_set",
                        "n2": ""}
    shorthand_qedges = {"e0": "n0--n1",
                        "e1": "n1--n2"}
    query_graph = _convert_shorthand_to_qg(shorthand_qnodes, shorthand_qedges)
    shorthand_kg_nodes = {"n0": [],
                          "n1": ["UniProtKB:123", "UniProtKB:124"],
                          "n2": ["DOID:122"]}
    shorthand_kg_edges = {"e0": [],
                          "e1": ["UniProtKB:123--DOID:122", "UniProtKB:124--DOID:122"]}
    knowledge_graph = _convert_shorthand_to_kg(shorthand_kg_nodes, shorthand_kg_edges)
    response, message = _run_resultify_directly(query_graph, knowledge_graph)
    assert response.status == 'OK'
    assert len(message.results) == 0


@pytest.mark.slow
def test_issue731b():
    actions = [
        "add_qnode(name=MONDO:0005737, key=n0, categories=biolink:Disease)",
        "add_qnode(categories=biolink:Protein, key=n1)",
        "add_qnode(categories=biolink:Disease, key=n2)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "add_qedge(subject=n1, object=n2, key=e1)",
        "expand(edge_key=[e0,e1], kp=infores:rtx-kg2)",
        "resultify(debug=true)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    for result in message.results:
        found_e01 = result.edge_bindings.get('e1')
        assert found_e01


def test_issue731c():
    qg = QueryGraph(nodes={'n0': QNode(ids='MONDO:0005737',
                                       categories='biolink:Disease'),
                           'n1': QNode(categories='biolink:Protein'),
                           'n2': QNode(categories='biolink:Disease')},
                    edges={'e0': QEdge(subject='n0',
                                       object='n1'),
                           'e1': QEdge(subject='n1',
                                       object='n2')})
    kg_node_info = ({'node_key': 'MONDO:0005737',
                     'categories': 'disease',
                     'qnode_keys': ['n0']},
                    {'node_key': 'UniProtKB:Q14943',
                     'categories': 'protein',
                     'qnode_keys': ['n1']},
                    {'node_key': 'DOID:12297',
                     'categories': 'disease',
                     'qnode_keys': ['n2']},
                    {'node_key': 'DOID:11077',
                     'categories': 'disease',
                     'qnode_keys': ['n2']})
    kg_edge_info = ({'edge_key': 'UniProtKB:Q14943--MONDO:0005737',
                     'object': 'MONDO:0005737',
                     'subject': 'UniProtKB:Q14943',
                     'qedge_keys': ['e0']},
                    {'edge_key': 'DOID:12297--UniProtKB:Q14943',
                     'object': 'UniProtKB:Q14943',
                     'subject': 'DOID:12297',
                     'qedge_keys': ['e1']})

    kg_nodes = _create_nodes(kg_node_info)
    kg_edges = _create_edges(kg_edge_info)

    kg = KnowledgeGraph(nodes=kg_nodes, edges=kg_edges)
    results = ARAX_resultify._get_results_for_kg_by_qg(kg, qg)
    indexes_results_with_single_edge = [index for index, result in enumerate(results) if len(result.edge_bindings) == 1]
    assert len(indexes_results_with_single_edge) == 0


def test_issue740():
    # Tests that self-edges are handled properly
    shorthand_qnodes = {"n00": "",
                        "n01": ""}
    shorthand_qedges = {"e00": "n00--n01"}
    query_graph = _convert_shorthand_to_qg(shorthand_qnodes, shorthand_qedges)
    shorthand_kg_nodes = {"n00": ["UMLS:C0004572"],  # Babesia
                          "n01": ["HP:01", "HP:02", "UMLS:C0004572"]}
    shorthand_kg_edges = {"e00": ["UMLS:C0004572--HP:01", "UMLS:C0004572--HP:02", "UMLS:C0004572--UMLS:C0004572"]}
    knowledge_graph = _convert_shorthand_to_kg(shorthand_kg_nodes, shorthand_kg_edges)
    response, message = _run_resultify_directly(query_graph, knowledge_graph)
    assert response.status == 'OK'
    assert len(message.results) == 3


def test_issue692():
    kg = KnowledgeGraph(nodes=dict(),
                        edges=dict())
    qg = QueryGraph(nodes=dict(),
                    edges=dict())
    results_list = ARAX_resultify._get_results_for_kg_by_qg(kg, qg)
    assert len(results_list) == 0


def test_issue692b():
    query_graph = QueryGraph(nodes=dict(), edges=dict())
    knowledge_graph = KnowledgeGraph(nodes=dict(), edges=dict())
    response, message = _run_resultify_directly(query_graph, knowledge_graph)
    assert 'no results returned; empty knowledge graph' in response.messages_list()[0]['message']


@pytest.mark.slow
def test_issue720_1():
    # Test when same node fulfills different qnode_keys within same result
    actions = [
        "add_qnode(ids=DOID:14330, key=n00)",
        "add_qnode(categories=biolink:Protein, ids=[UniProtKB:Q02878, UniProtKB:Q9BXM7], is_set=true, key=n01)",
        "add_qnode(categories=biolink:Disease, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01)",
        "expand(kp=infores:rtx-kg2)",
        "resultify(debug=true)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    n02_nodes_in_kg = [node for node in message.knowledge_graph.nodes.values() if "n02" in node.qnode_keys]
    assert message.results and len(message.results) >= len(n02_nodes_in_kg)
    assert response.status == 'OK'


@pytest.mark.slow
def test_issue720_2():
    # Test when same node fulfills different qnode_keys within same result
    actions = [
        "add_qnode(ids=UMLS:C0158779, key=n00)",
        "add_qnode(ids=UMLS:C0578454, key=n01)",
        "add_qnode(key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01)",
        "expand(kp=infores:rtx-kg2)",
        "resultify(debug=true)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    n02_nodes_in_kg = [node for node in message.knowledge_graph.nodes.values() if "n02" in node.qnode_keys]
    assert message.results and len(message.results) == len(n02_nodes_in_kg)
    assert response.status == 'OK'


@pytest.mark.slow
def test_issue720_3():
    # Tests when same node fulfills different qnode_keys in different results
    actions = [
        "add_qnode(key=n00, ids=DOID:14330)",  # parkinson's
        "add_qnode(key=n01, categories=biolink:Protein)",
        "add_qnode(key=n02, categories=biolink:ChemicalEntity, ids=CHEMBL.COMPOUND:CHEMBL1489)",
        "add_qnode(key=n03, categories=biolink:Protein)",
        "add_qedge(key=e00, subject=n01, object=n00, predicates=biolink:causes)",
        "add_qedge(key=e01, subject=n01, object=n02, predicates=biolink:interacts_with)",
        "add_qedge(key=e02, subject=n02, object=n03, predicates=biolink:interacts_with)",
        "expand(kp=infores:rtx-kg2)",
        "resultify(debug=true)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    n03s = {node_binding.id for result in message.results for node_binding in result.node_bindings["n03"]}
    protein_presence = {node_id: {"n01_and_not_n03": False, "n03_and_not_n01": False} for node_id in n03s}
    for result in message.results:
        n01s = {node_binding.id for node_binding in result.node_bindings["n01"]}
        n03s = {node_binding.id for node_binding in result.node_bindings["n03"]}
        n01s_and_not_n03s = n01s.difference(n03s)
        n03s_and_not_n01s = n03s.difference(n01s)
        for node_id in n01s_and_not_n03s:
            protein_presence[node_id]["n01_and_not_n03"] = True
        for node_id in n03s_and_not_n01s:
            protein_presence[node_id]["n03_and_not_n01"] = True
    assert any(item for item in protein_presence.values() if item["n01_and_not_n03"] and item["n03_and_not_n01"])


def test_issue833_extraneous_intermediate_nodes():
    # Test for extraneous intermediate nodes
    shorthand_qnodes = {"n00": "",
                        "n01": "is_set",
                        "n02": "is_set",
                        "n03": ""}
    shorthand_qedges = {"e00": "n00--n01",
                        "e01": "n01--n02",
                        "e02": "n02--n03"}
    query_graph = _convert_shorthand_to_qg(shorthand_qnodes, shorthand_qedges)
    shorthand_kg_nodes = {"n00": ["DOID:1056"],
                          "n01": ["UniProtKB:111", "UniProtKB:222"],
                          "n02": ["MONDO:111", "MONDO:222"],  # Last one is dead-end
                          "n03": ["CHEBI:111"]}
    shorthand_kg_edges = {"e00": ["DOID:1056--UniProtKB:111", "DOID:1056--UniProtKB:222"],
                          "e01": ["UniProtKB:111--MONDO:111", "UniProtKB:222--MONDO:222"],
                          "e02": ["MONDO:111--CHEBI:111"]}
    knowledge_graph = _convert_shorthand_to_kg(shorthand_kg_nodes, shorthand_kg_edges)
    response, message = _run_resultify_directly(query_graph, knowledge_graph)
    assert response.status == 'OK'
    for result in message.results:
        result_n01_nodes = {node_binding.id for node_binding in result.node_bindings["n01"]}
        result_e01_edges = {edge_binding.id for edge_binding in result.edge_bindings["e01"]}
        result_e00_edges = {edge_binding.id for edge_binding in result.edge_bindings["e00"]}
        for n01_node_key in result_n01_nodes:
            kg_edges_using_this_node = _get_kg_edge_keys_using_node(n01_node_key, message.knowledge_graph)
            assert result_e01_edges.intersection(kg_edges_using_this_node)
            assert result_e00_edges.intersection(kg_edges_using_this_node)


def test_single_node():
    actions = [
        "add_qnode(name=ibuprofen, key=n00)",
        "expand(node_key=n00, kp=infores:rtx-kg2)",
        "resultify(debug=true)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert len(message.results) > 0


def test_parallel_edges_between_nodes():
    qg_nodes = {"n00": "",
                "n01": "is_set",
                "n02": ""}
    qg_edges = {"e00": "n00--n01",
                "e01": "n01--n02",
                "parallel01": "n01--n02"}
    query_graph = _convert_shorthand_to_qg(qg_nodes, qg_edges)
    kg_nodes = {"n00": ["DOID:11830"],
                "n01": ["UniProtKB:P39060", "UniProtKB:P20849"],
                "n02": ["CHEBI:85164", "CHEBI:29057"]}
    kg_edges = {"e00": ["DOID:11830--UniProtKB:P39060", "DOID:11830--UniProtKB:P20849"],
                "e01": ["UniProtKB:P39060--CHEBI:85164", "UniProtKB:P20849--CHEBI:29057"],
                "parallel01": ["UniProtKB:P39060--CHEBI:85164", "UniProtKB:P20849--CHEBI:29057", "UniProtKB:P39060--CHEBI:29057"]}
    kg_before_resultify = _convert_shorthand_to_kg(kg_nodes, kg_edges)
    response, message = _run_resultify_directly(query_graph, kg_before_resultify)
    kg = message.knowledge_graph
    assert response.status == 'OK'
    n02_nodes = {node_key for node_key, node in kg.nodes.items() if "n02" in node.qnode_keys}
    assert message.results and len(message.results) == len(n02_nodes)
    # Make sure every n01 node is connected to both an e01 edge and a parallel01 edge in each result
    for result in message.results:
        result_node_keys_by_qg_key = _get_result_node_keys_by_qg_key(result)
        result_edge_keys_by_qg_key = _get_result_edge_keys_by_qg_key(result)
        node_keys_used_by_e01_edges = {node_key for edge_key in result_edge_keys_by_qg_key['e01']
                                      for node_key in {kg.edges[edge_key].subject, kg.edges[edge_key].object}}
        node_keys_used_by_parallel01_edges = {node_key for edge_key in result_edge_keys_by_qg_key['parallel01']
                                             for node_key in {kg.edges[edge_key].subject, kg.edges[edge_key].object}}
        for node_key in result_node_keys_by_qg_key['n01']:
            assert node_key in node_keys_used_by_e01_edges
            assert node_key in node_keys_used_by_parallel01_edges


def test_issue912_clean_up_kg():
    # Tests that the returned knowledge graph contains only nodes used in the results
    qg_nodes = {"n00": "",
                "n01": "is_set",
                "n02": ""}
    qg_edges = {"e00": "n00--n01",
                "e01": "n01--n02"}
    query_graph = _convert_shorthand_to_qg(qg_nodes, qg_edges)
    kg_nodes = {"n00": ["DOID:11", "DOID:NotConnected"],
                "n01": ["PR:110", "PR:111", "PR:DeadEnd"],
                "n02": ["CHEBI:11", "CHEBI:NotConnected"]}
    kg_edges = {"e00": ["DOID:11--PR:110", "DOID:11--PR:111", "DOID:11--PR:DeadEnd"],
                "e01": ["PR:110--CHEBI:11", "PR:111--CHEBI:11"]}
    knowledge_graph = _convert_shorthand_to_kg(kg_nodes, kg_edges)
    response, message = _run_resultify_directly(query_graph, knowledge_graph)
    assert response.status == 'OK'
    assert len(message.results) == 1
    returned_kg_node_keys = set(message.knowledge_graph.nodes)
    assert returned_kg_node_keys == {"DOID:11", "PR:110", "PR:111", "CHEBI:11"}
    orphan_edges = {edge_key for edge_key, edge in message.knowledge_graph.edges.items()
                    if not {edge.subject, edge.object}.issubset(returned_kg_node_keys)}
    assert not orphan_edges


@pytest.mark.slow
def test_issue1119_a():
    # Run a query to identify chemical substances that are both indicated for and contraindicated for our disease
    actions = [
        "add_qnode(name=DOID:3312, key=n00, is_set=True)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n01, object=n00, predicates=biolink:treats, key=e00)",
        "add_qedge(subject=n01, object=n00, predicates=biolink:predisposes, key=e01)",
        "expand(kp=infores:rtx-kg2)",
        "resultify()",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert message.results
    contraindicated_pairs = {tuple(sorted([edge.subject, edge.object])) for edge in message.knowledge_graph.edges.values()}

    # Verify those chemical substances aren't returned when we make the contraindicated_for edge kryptonite
    actions = [
        "add_qnode(name=DOID:3312, key=n00, is_set=True)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n01, object=n00, predicates=biolink:treats, key=e00)",
        "add_qedge(subject=n01, object=n00, predicates=biolink:predisposes, exclude=true, key=ex0)",
        "expand(kp=infores:rtx-kg2)",
        "resultify()",
        "return(message=true, store=false)"
    ]
    kryptonite_response, kryptonite_message = _do_arax_query(actions)
    assert kryptonite_response.status == 'OK'
    assert kryptonite_message.results
    all_pairs = {tuple(sorted([edge.subject, edge.object])) for edge in kryptonite_message.knowledge_graph.edges.values()}
    assert not contraindicated_pairs.intersection(all_pairs)


@pytest.mark.slow
def test_issue1119_b():
    # Tests a perpendicular kryptonite qedge situation
    actions = [
        "add_qnode(ids=DOID:3312, key=n00)",
        "add_qnode(categories=biolink:Protein, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:treats)",
        "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:physically_interacts_with)",
        "add_qnode(categories=biolink:Pathway, key=n03)",
        "add_qedge(subject=n01, object=n03, key=e02, predicates=biolink:participates_in, exclude=true)",
        "expand(kp=infores:rtx-kg2)",
        "resultify()",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert message.results
    # Make sure the kryptonite edge and its leaf qnode don't appear in any results
    assert not any(result.node_bindings.get("n03") for result in message.results)
    assert not any(result.edge_bindings.get("e02") for result in message.results)


@pytest.mark.slow
def test_issue1119_c():
    # Test a simple one-hop query with one single-edge option group
    actions = [
        "add_qnode(key=n00, ids=DOID:3312)",
        "add_qnode(key=n01, categories=biolink:ChemicalEntity)",
        "add_qedge(key=e00, subject=n01, object=n00, predicates=biolink:causes)",
        "add_qedge(key=e01, subject=n01, object=n00, predicates=biolink:predisposes, option_group_id=1)",
        "expand(kp=infores:rtx-kg2)",
        "resultify(debug=true)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert message.results
    assert all(result.edge_bindings.get("e00") for result in message.results)
    # Make sure at least one of our results has the "optional" group 1 edge
    results_with_optional_edge = [result for result in message.results if result.edge_bindings.get("e01")]
    assert results_with_optional_edge

    # Make sure the number of results is the same as if we asked only for the required portion
    actions = [
        "add_qnode(key=n00, ids=DOID:3312)",
        "add_qnode(key=n01, categories=biolink:ChemicalEntity)",
        "add_qedge(key=e00, subject=n01, object=n00, predicates=biolink:causes)",
        "expand(kp=infores:rtx-kg2)",
        "resultify(debug=true)",
        "return(message=true, store=false)"
    ]
    response, message_without_option_group = _do_arax_query(actions)
    assert response.status == 'OK'
    assert len(message_without_option_group.results) == len(message.results)

    # And make sure the number of results with an option group edge makes sense
    actions = [
        "add_qnode(key=n00, ids=DOID:3312)",
        f"add_qnode(key=n01, ids=[{', '.join([node_key for node_key, node in message.knowledge_graph.nodes.items() if 'n01' in node.qnode_keys])}])",
        "add_qedge(key=e00, subject=n01, object=n00, predicates=biolink:predisposes)",
        "expand(kp=infores:rtx-kg2)",
        "return(message=true, store=false)"
        # Note: skipping resultify here due to issue #1152
    ]
    response, message_option_edge_only = _do_arax_query(actions)
    assert response.status == 'OK'
    assert len(results_with_optional_edge) == len([node for node in message_option_edge_only.knowledge_graph.nodes.values()
                                                   if "n01" in node.qnode_keys])


@pytest.mark.slow
def test_issue1119_d():
    # Test one-hop query with multiple single-edge option groups and a required 'not' edge
    actions = [
        "add_qnode(key=n00, ids=DOID:3312)",
        "add_qnode(key=n01, categories=biolink:ChemicalEntity)",
        "add_qedge(key=e00, subject=n01, object=n00, predicates=biolink:treats)",
        "add_qedge(key=e01, subject=n01, object=n00, predicates=biolink:affects, option_group_id=1)",
        "add_qedge(key=e02, subject=n01, object=n00, predicates=biolink:disrupts, option_group_id=2)",
        "add_qedge(key=e03, subject=n01, object=n00, exclude=True, predicates=biolink:predisposes)",
        "expand(kp=infores:rtx-kg2)",
        "resultify(debug=true)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert message.results
    # Make sure every result has a required edge
    assert all(result.edge_bindings.get("e00") for result in message.results)
    assert not any(result.edge_bindings.get("e03") for result in message.results)
    # Make sure our "optional" edges appear in one or more results
    assert any(result for result in message.results if result.edge_bindings.get("e01"))
    assert any(result for result in message.results if result.edge_bindings.get("e02"))
    # Verify there are some results without any optional portion (happens to be true for this query)
    assert any(result for result in message.results if not {"e01", "e02"}.issubset(set(result.edge_bindings)))


@pytest.mark.slow
def test_issue1146_a():
    actions = [
        "add_qnode(key=n0, ids=MONDO:0008380, categories=biolink:Disease)",
        "add_qnode(key=n2, categories=biolink:ChemicalEntity)",
        "add_qnode(key=n1, categories=biolink:Protein, is_set=true)",
        "add_qedge(key=e0, subject=n2, object=n1, predicates=biolink:physically_interacts_with)",
        "add_qedge(key=e1, subject=n1, object=n0, predicates=biolink:causes)",
        "expand(kp=infores:rtx-kg2)",
        "overlay(action=compute_ngd, virtual_relation_label=N2, subject_qnode_key=n0, object_qnode_key=n2)",
        "resultify(debug=true)",
        "filter_results(action=limit_number_of_results, max_results=4)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert len(message.results) == 4
    # Make sure every n1 node is connected to an e1 and e0 edge
    for result in message.results:
        result_n1_nodes = {node_binding.id for node_binding in result.node_bindings["n1"]}
        result_e1_edges = {edge_binding.id for edge_binding in result.edge_bindings["e1"]}
        result_e0_edges = {edge_binding.id for edge_binding in result.edge_bindings["e0"]}
        for n1_node in result_n1_nodes:
            kg_edges_using_this_node = _get_kg_edge_keys_using_node(n1_node, message.knowledge_graph)
            assert result_e1_edges.intersection(kg_edges_using_this_node)
            assert result_e0_edges.intersection(kg_edges_using_this_node)


def test_disconnected_qg():
    # Ensure an (informative) error is thrown when the QG is disconnected (has more than one component)
    actions = [
        "add_qnode(name=ibuprofen, key=n00)",
        "add_qnode(name=acetaminophen, key=n01)",
        "add_qnode(categories=biolink:Disease, key=n02)",
        "add_qedge(key=e00, subject=n01, object=n02)",
        "expand(kp=infores:rtx-kg2)",
        "resultify(debug=true)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status != 'OK'
    assert "QG is disconnected" in response.show()


def test_recompute_qg_keys():
    shorthand_qnodes = {"n00": "",
                        "n01": ""}
    shorthand_qedges = {"e00": "n00--n01"}
    query_graph = _convert_shorthand_to_qg(shorthand_qnodes, shorthand_qedges)
    shorthand_kg_nodes = {"n00": ["DOID:731"],
                          "n01": ["HP:01", "HP:02", "HP:03", "HP:04"]}
    shorthand_kg_edges = {"e00": ["DOID:731--HP:01", "DOID:731--HP:02", "DOID:731--HP:03", "DOID:731--HP:04"]}
    knowledge_graph = _convert_shorthand_to_kg(shorthand_kg_nodes, shorthand_kg_edges)
    response, message = _run_resultify_directly(query_graph, knowledge_graph)
    assert response.status == 'OK'
    assert message.results
    # Clear all qnode_keys/qedge_keys from the KG
    for node_key, node in message.knowledge_graph.nodes.items():
        node.qnode_keys = []
    for edge_key, edge in message.knowledge_graph.edges.items():
        edge.qedge_keys = []
    # Then recompute qg keys and make sure look ok
    resultifier = ARAXResultify()
    resultifier.recompute_qg_keys(response)
    assert response.status == 'OK'
    kg = response.envelope.message.knowledge_graph
    assert kg.nodes and kg.edges
    for node_key, node in kg.nodes.items():
        assert node.qnode_keys == ["n00"] if node_key in shorthand_kg_nodes["n00"] else ["n01"]
    for edge_key, edge in kg.edges.items():
        assert edge.qedge_keys == ["e00"]


def test_multi_node_edgeless_qg():
    shorthand_qnodes = {"n00": "",
                        "n01": ""}
    shorthand_qedges = {}
    query_graph = _convert_shorthand_to_qg(shorthand_qnodes, shorthand_qedges)
    shorthand_kg_nodes = {"n00": ["CHEMBL.COMPOUND:CHEMBL635"],
                          "n01": ["MESH:D052638"]}
    shorthand_kg_edges = {}
    knowledge_graph = _convert_shorthand_to_kg(shorthand_kg_nodes, shorthand_kg_edges)
    response, message = _run_resultify_directly(query_graph, knowledge_graph)
    assert response.status == 'OK'
    assert len(message.results) == 1


@pytest.mark.slow
def test_issue_1446():
    actions = [
        "add_qnode(ids=HGNC:6284, key=n0, categories=biolink:Gene)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(key=e0,subject=n0,object=n1, predicates=biolink:entity_negatively_regulates_entity)",
        "add_qedge(key=e1,subject=n0,object=n1, predicates=biolink:decreases_activity_of, option_group_id=1)",
        "add_qedge(key=e2,subject=n0,object=n1, predicates=biolink:decreases_expression_of, option_group_id=2)",
        "expand(kp=infores:rtx-kg2)",
        "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n0, object_qnode_key=n1)",
        "resultify()",
        "filter_results(action=limit_number_of_results, max_results=100)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == "OK"
    assert message.results


@pytest.mark.slow
def test_issue_1848():
    # Verifies that only the part of the QG that's already been expanded is resultified
    actions = [
        "add_qnode(key=n0, ids=MONDO:0019391)",
        "add_qnode(key=n1, categories=biolink:Gene)",
        "add_qedge(key=e0, subject=n1, object=n0, predicates=biolink:causes)",
        "expand(kp=infores:rtx-kg2)",
        "add_qnode(key=n2, categories=biolink:Drug)",
        "add_qedge(key=e1, subject=n1, object=n2)",
        "resultify()",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    kg = response.envelope.message.knowledge_graph
    assert response.status == "OK"
    assert kg.nodes
    assert kg.edges
    assert message.results
    qedge_bindings_in_kg = {qedge_key for edge in kg.edges.values() for qedge_key in edge.qedge_keys}
    non_subclass_qedge_bindings_in_kg = {qedge_key for qedge_key in qedge_bindings_in_kg if not qedge_key.startswith("subclass:")}
    assert non_subclass_qedge_bindings_in_kg == {"e0"}


def test_node_binding_query_id_one_hop_single_input_curie():
    diabetes_curie = "MONDO:0005015"
    type_1_diabetes_curie = "MONDO:0005147"
    actions = [
        f"add_qnode(ids={diabetes_curie}, key=n00)",
        f"add_qnode(categories=biolink:Drug, key=n01)",
        "add_qedge(subject=n01, object=n00, predicates=biolink:treats, key=e00)",
        "expand(kp=infores:rtx-kg2)",
        "resultify(debug=true)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert len(message.results) > 1
    kg = response.envelope.message.knowledge_graph
    # Make the input curie and one of its children appear somewhere in the results
    assert diabetes_curie in kg.nodes
    assert type_1_diabetes_curie in kg.nodes
    # Make sure node bindings do/don't have 'query_id' filled out as appropriate
    for result in message.results:
        for node_binding in result.node_bindings["n00"]:
            # No nodes should have query_id filled out per TRAPI because it's obvious what they map to (diabetes)
            assert node_binding.query_id is None
        for node_binding in result.node_bindings["n01"]:
            assert node_binding.query_id is None
    # Make sure we have some results with subclass self-edges (Expand assigns such qedges keys like 'subclass:n00-n00')
    assert any(qedge_key.startswith("subclass")
               for result in message.results
               for qedge_key in result.edge_bindings)
    insulin_results = [result for result in message.results if result.essence == "Insulin"]
    assert len(insulin_results) == 1


def test_node_binding_query_id_one_hop_multiple_input_curies():
    diabetes_curie = "MONDO:0005015"
    type_1_diabetes_curie = "MONDO:0005147"
    parent_query_ids = {diabetes_curie, type_1_diabetes_curie}
    actions = [
        f"add_qnode(ids=[{','.join(parent_query_ids)}], key=n00)",
        f"add_qnode(categories=biolink:Drug, key=n01)",
        "add_qedge(subject=n01, object=n00, predicates=biolink:treats, key=e00)",
        "expand(kp=infores:rtx-kg2)",
        "resultify(debug=true)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert len(message.results) > 1
    kg = response.envelope.message.knowledge_graph
    # Make sure both input curies appear somewhere in the results
    assert diabetes_curie in kg.nodes
    # TODO: Do the below check after we've figured out the multiple query IDs problem (nodes could fulfill either
    #  diabetes or type 1 diabetes), but can only have 1 n00 parent specified
    # assert type_1_diabetes_curie in kg.nodes
    # Make sure node bindings do/don't have 'query_id' filled out as appropriate
    for result in message.results:
        for node_binding in result.node_bindings["n00"]:
            if node_binding.id in parent_query_ids:
                assert node_binding.query_id is None
            else:
                assert node_binding.query_id in parent_query_ids
        for node_binding in result.node_bindings["n01"]:
            assert node_binding.query_id is None
    # Make sure we have some results with subclass self-edges (Expand assigns such qedges keys like 'subclass:n00-n00')
    assert any(qedge_key.startswith("subclass")
               for result in message.results
               for qedge_key in result.edge_bindings)
    insulin_results = [result for result in message.results if result.essence == "Insulin"]
    assert len(insulin_results) in range(1, 3)


@pytest.mark.slow
def test_node_binding_query_id_two_hop_double_pinned():
    diabetes_curie = "MONDO:0005015"
    heart_disease_curie = "MONDO:0005267"
    actions = [
        f"add_qnode(ids={diabetes_curie}, key=n00)",
        f"add_qnode(ids={heart_disease_curie}, key=n01)",
        f"add_qnode(categories=biolink:Drug, key=n02)",
        "add_qedge(subject=n01, object=n00, predicates=biolink:related_to, key=e00)",
        "add_qedge(subject=n01, object=n02, predicates=biolink:treats, key=e01)",
        "expand(kp=infores:rtx-kg2)",
        "resultify(debug=true)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert len(message.results) > 1
    kg = response.envelope.message.knowledge_graph
    # Make the input curie and one of its children appear somewhere in the results
    assert diabetes_curie in kg.nodes
    assert heart_disease_curie in kg.nodes
    # Make sure node bindings do/don't have 'query_id' filled out as appropriate
    for result in message.results:
        for node_binding in result.node_bindings["n00"]:
            assert node_binding.query_id is None  # Mapping implied because only single input curie
        for node_binding in result.node_bindings["n01"]:
            assert node_binding.query_id is None  # Mapping implied because only single input curie
        for node_binding in result.node_bindings["n02"]:
            assert node_binding.query_id is None
    # Make sure there's one result for Dabigatran and its structure is as expected
    dabigatran_results = [result for result in message.results if result.essence == "DABIGATRAN"]
    assert len(dabigatran_results) == 1
    dabigatran_result = dabigatran_results[0]
    edge_keys_that_should_be_filled = {"e00", "e01", "subclass:n00--n00", "subclass:n01--n01"}
    assert set(dabigatran_result.edge_bindings) == edge_keys_that_should_be_filled
    for edge_key in edge_keys_that_should_be_filled:
        assert len(dabigatran_result.edge_bindings[edge_key])
    # Make sure we have some results with BOTH subclass self-edges
    num_results_with_both_subclass_qedges = 0
    for result in message.results:
        subclass_qedges_present = set()
        for qedge_key in result.edge_bindings:
            if qedge_key.startswith("subclass"):
                subclass_qedges_present.add(qedge_key)
        if len(subclass_qedges_present) > 1:
            num_results_with_both_subclass_qedges += 1
    assert num_results_with_both_subclass_qedges > 1


@pytest.mark.external
def test_missing_chp_results():
    # Note: for this test to pass, need to use a maturity that CHP has an endpoint for (they don't have dev currently)
    uberon_curies = ["UBERON:0009912", "UBERON:0002535", "UBERON:0000019", "UBERON:0002365", "UBERON:0000017",
                     "UBERON:0000970", "UBERON:0001831", "UBERON:0016410", "UBERON:0001737", "UBERON:0000945"]
    actions = [
        f"add_qnode(ids=[{','.join(uberon_curies)}], categories=biolink:GrossAnatomicalStructure, key=n1)",
        f"add_qnode(categories=biolink:Gene, key=n2)",
        "add_qedge(subject=n1, object=n2, predicates=biolink:expresses, key=e1)",
        "expand(kp=infores:connections-hypothesis)",
        "resultify(debug=true)",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert len(message.results) > 20
    assert any(result for result in message.results if "subclass:n1--n1" in result.edge_bindings)


@pytest.mark.slow
@pytest.mark.external
def test_too_few_results():
    # Note: for this test to pass, need to use a maturity that CHP has an endpoint for (they don't have dev currently)
    actions = [
        "add_qnode(key=n0, ids=MONDO:0009061, categories=biolink:Disease)",
        "add_qnode(key=n1, categories=biolink:GrossAnatomicalStructure)",
        "add_qnode(key=n2, categories=biolink:Gene)",
        "add_qnode(key=n3, categories=[biolink:Drug, biolink:SmallMolecule])",
        "add_qedge(key=e0, subject=n0, object=n1, predicates=biolink:located_in)",
        "add_qedge(key=e1, subject=n1, object=n2, predicates=biolink:expresses)",
        "add_qedge(key=e2, subject=n3, object=n2, predicates=biolink:affects)",
        "expand(edge_key=e0, prune_threshold=1000, kp_timeout=75)",
        "expand(edge_key=e1, kp=infores:connections-hypothesis, prune_threshold=1000, kp_timeout=75)",
        "expand(edge_key=e2, prune_threshold=1000, kp_timeout=75)",
        "resultify()",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'
    assert len(message.results) > 200


@pytest.mark.slow
@pytest.mark.external
def test_issue1923_multiple_essence_candidates_subclass():
    actions = [
        "add_qnode(name=ATP1A3, key=n0)",
        "add_qnode(categories=biolink:PhenotypicFeature, key=n1)",
        "add_qnode(categories=biolink:Protein, key=n2)",
        "add_qnode(categories=biolink:ChemicalSubstance, key=n3)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "add_qedge(subject=n1, object=n2, key=e1)",
        "add_qedge(subject=n2, object=n3, key=e2)",
        "expand(prune_threshold=50, kp_timeout=30)",
        "resultify()",
        "return(message=true, store=false)"
    ]
    response, message = _do_arax_query(actions)
    assert response.status == 'OK'


if __name__ == '__main__':
    pytest.main(['-v', 'test_ARAX_resultify.py'])


# code/ARAX/test/test_ARAX_standup_queries.py

#!/usr/bin/env python3

# Intended to test our more complicated workflows

import sys
import os
import pytest
from collections import Counter
import copy
import json
import ast
from typing import List, Union, Optional

import numpy as np

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../ARAXQuery")
sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
from ARAX_query import ARAXQuery
from ARAX_response import ARAXResponse

PACKAGE_PARENT = '../../UI/OpenAPI/openapi_server'
sys.path.append(os.path.normpath(os.path.join(os.getcwd(), PACKAGE_PARENT)))
from openapi_server.models.message import Message


def _do_arax_query(query: dict, timeout: Optional[int] = None) -> List[Union[ARAXResponse, Message]]:
    araxq = ARAXQuery()
    if timeout:
        query["query_options"] = {"kp_timeout": timeout}
    response = araxq.query(query)
    if response.status != 'OK':
        print(response.show(level=response.DEBUG))
    return [response, response.envelope.message]


def _attribute_tester(message, attribute_name: str, attribute_type: str, num_different_values=2):
    """
    Tests attributes of a message
    message: returned from _do_arax_query
    attribute_name: the attribute name to test (eg. 'jaccard_index')
    attribute_type: the attribute type (eg. 'EDAM:data_1234')
    num_different_values: the number of distinct values you wish to see have been added as attributes
    """
    edges_of_interest = []
    values = set()
    for key, edge in message.knowledge_graph.edges.items():
        if hasattr(edge, 'edge_attributes'):
            for attr in edge.edge_attributes:
                if attr.name == attribute_name:
                    edges_of_interest.append(edge)
                    assert attr.type == attribute_type
                    values.add(attr.value)
    assert len(edges_of_interest) > 0
    assert len(values) >= num_different_values


def _virtual_tester(message: Message, edge_predicate: str, relation: str, attribute_name: str, attribute_type: str, num_different_values=2):
    """
    Tests overlay functions that add virtual edges
    message: returned from _do_arax_query
    edge_predicate: the name of the virtual edge (eg. biolink:has_jaccard_index_with)
    relation: the relation you picked for the virtual_edge_relation (eg. N1)
    attribute_name: the attribute name to test (eg. 'jaccard_index')
    attribute_type: the attribute type (eg. 'EDAM:data_1234')
    num_different_values: the number of distinct values you wish to see have been added as attributes
    """
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert edge_predicate in edge_predicates_in_kg
    edges_of_interest = [x for x in message.knowledge_graph.edges.values() if x.relation == relation]
    values = set()
    assert len(edges_of_interest) > 0
    for edge in edges_of_interest:
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        assert edge.attributes[0].name == attribute_name
        values.add(edge.attributes[0].value)
        assert edge.attributes[0].type == attribute_type
    # make sure two or more values were added
    assert len(values) >= num_different_values


def test_gene_to_pathway_issue_9():
    query = {
        "message": {
            "query_graph": {
                "nodes": {
                    "n0": {
                        "ids": ["NCBIGENE:1017"],
                        "categories": ["biolink:Gene"]
                    },
                    "n1": {
                        
                        "categories": ["biolink:Pathway"]
                    }
                },
                "edges": {
                    "e01": {
                        "subject": "n0",
                        "object": "n1"
                    }
                }
            }
        }
    }
    [response, message] = _do_arax_query(query, timeout=30)
    assert response.status == 'OK'
    assert len(message.results) > 0


def test_chemicals_to_gene_issue_10():
    query = {
        "message": {
            "query_graph": {
                "nodes": {
                    "n0": {
                        "ids": ["UniProtKB:P52788"],
                        "categories":["biolink:Gene"]
                    },
                    "n1": {
                        "categories": ["biolink:ChemicalEntity"]
                    }
                },
                "edges": {
                    "e01": {
                        "subject": "n0",
                        "object": "n1"
                    }
                }
            }
        }
    }
    [response, message] = _do_arax_query(query, timeout=30)
    assert response.status == 'OK'
    assert len(message.results) > 0


def test_named_thing_associated_with_acrocynaosis_issue_12():
    query = {
        "message": {
            "query_graph": {
                "nodes": {
                    "n0": {
                        "ids": ["UMLS:C0221347"],
                        "categories":["biolink:PhenotypicFeature"]
                    },
                    "n1": {
                        "categories": ["biolink:NamedThing"]
                    }
                },
                "edges": {
                    "e01": {
                        "subject": "n0",
                        "object": "n1"
                    }
                }
            }
        }
    }
    [response, message] = _do_arax_query(query, timeout=30)
    assert response.status == 'OK'
    assert len(message.results) > 0


@pytest.mark.external
def test_chemical_substances_correlated_with_asthma_issue_18():
    query = {
      "message": {
        "query_graph": {
          "nodes": {
            "n0": {
              "ids": ["MONDO:0004979"],
              "categories": ["biolink:Disease"]
            },
            "n1": {
              "categories": ["biolink:ChemicalEntity"]
            }
          },
          "edges": {
            "e01": {
              "subject": "n0",
              "object": "n1",
              "predicates": ["biolink:correlated_with"]
            }
          }
        }
      }
    }
    [response, message] = _do_arax_query(query, timeout=30)
    assert response.status == 'OK'
    assert len(message.results) > 0


def test_diseases_treated_by_drug_issue_20():
    query = {
      "message": {
        "query_graph": {
          "edges": {
            "e01": {
              "object": "n0",
              "predicates": ["biolink:treated_by"],
              "subject": "n1"
            }
          },
          "nodes": {
            "n0": {
              "categories": ["biolink:Drug"],
              "ids": ["DRUGBANK:DB00394"]
            },
            "n1": {
              "categories": ["biolink:Disease"]
            }
          }
        }
      }
     }
    [response, message] = _do_arax_query(query, timeout=30)
    assert response.status == 'OK'
    assert len(message.results) > 0


def test_chemical_substances_that_down_regulate_STK11_issue_28():
    query = {
      "message": {
        "query_graph": {
          "edges": {
            "e01": {
              "object": "n0",
              "predicates": ["biolink:prevents",
                "biolink:negatively_regulates",
                "biolink:decreases_secretion_of",
                "biolink:decreases_secretion_of",
                "biolink:decreases_transport_of",
                "biolink:decreases_activity_of",
                "biolink:decreases_synthesis_of",
                "biolink:decreases_expression_of",
                "biolink:increases_degradation_of",
                "biolink:entity_negatively_regulates_entity",
                "biolink:disrupts",
                "biolink:directly_negatively_regulates",
                "biolink:inhibits",
                "biolink:inhibitor",
                "biolink:channel_blocker",
                "biolink:disrupts",
                "biolink:may_inhibit_effect_of"
              ],
              "subject": "n1"
            }
          },
          "nodes": {
            "n0": {
              "ids": ["HGNC:11389"]
            },
            "n1": {
              "categories": ["biolink:ChemicalEntity"]
            }
          }
        }
      }
     }
    [response, message] = _do_arax_query(query, timeout=30)
    assert response.status == 'OK'
    assert len(message.results) > 0


# This query doesn't find results after conflations were resolved in KG2.6.7
@pytest.mark.skip
def test_phenotypes_for_angel_shaped_phalango_epiphyseal_dysplasia_issue_33():
    query = {
      "message": {
        "query_graph": {
          "edges": {
            "e01": {
              "object": "n0",
              "subject": "n1",
              "predicates":["biolink:has_phenotype"]
            }
          },
          "nodes": {
            "n0": {
              "ids": ["MONDO:0007114"],
              "categories":["biolink:Disease"]
            },
            "n1": {
              "categories": ["biolink:PhenotypicFeature"]
            }
          }
        }
      }
     }
    [response, message] = _do_arax_query(query, timeout=30)
    assert response.status == 'OK'
    assert len(message.results) > 0


if __name__ == "__main__":
    pytest.main(['-v'])



# code/ARAX/test/test_ARAX_translate.py

#!/usr/bin/env python3

# Intended to test our translate to ARAXi functionality

import sys
import os
import pytest
from collections import Counter
import copy
import json
import ast
from typing import List, Union

import numpy as np

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../ARAXQuery")
sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
from ARAX_query import ARAXQuery
from ARAX_response import ARAXResponse

PACKAGE_PARENT = '../../UI/OpenAPI/openapi_server'
sys.path.append(os.path.normpath(os.path.join(os.getcwd(), PACKAGE_PARENT)))
from openapi_server.models.message import Message


def _do_arax_query(query: dict) -> List[Union[ARAXResponse, Message]]:
    araxq = ARAXQuery()
    response = araxq.query(query)
    if response.status != 'OK':
        print(response.show(level=response.DEBUG))
    return [response, response.envelope.message]


def _attribute_tester(message, attribute_name: str, attribute_type: str, num_different_values=2):
    """
    Tests attributes of a message
    message: returned from _do_arax_query
    attribute_name: the attribute name to test (eg. 'jaccard_index')
    attribute_type: the attribute type (eg. 'EDAM:data_1234')
    num_different_values: the number of distinct values you wish to see have been added as attributes
    """
    edges_of_interest = []
    values = set()
    for key, edge in message.knowledge_graph.edges.items():
        if hasattr(edge, 'edge_attributes'):
            for attr in edge.edge_attributes:
                if attr.original_attribute_name == attribute_name:
                    edges_of_interest.append(edge)
                    assert attr.attribute_type_id == attribute_type
                    values.add(attr.value)
    assert len(edges_of_interest) > 0
    assert len(values) >= num_different_values


def _virtual_tester(message: Message, edge_predicate: str, relation: str, attribute_name: str, attribute_type: str, num_different_values=2):
    """
    Tests overlay functions that add virtual edges
    message: returned from _do_arax_query
    edge_predicate: the name of the virtual edge (eg. biolink:has_jaccard_index_with)
    relation: the relation you picked for the virtual_edge_relation (eg. N1)
    attribute_name: the attribute name to test (eg. 'jaccard_index')
    attribute_type: the attribute type (eg. 'EDAM:data_1234')
    num_different_values: the number of distinct values you wish to see have been added as attributes
    """
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert edge_predicate in edge_predicates_in_kg
    edges_of_interest = [x for x in message.knowledge_graph.edges.values() if x.relation == relation]
    values = set()
    assert len(edges_of_interest) > 0
    for edge in edges_of_interest:
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        assert edge.attributes[0].original_attribute_name == attribute_name
        values.add(edge.attributes[0].value)
        assert edge.attributes[0].attribute_type_id == attribute_type
    # make sure two or more values were added
    assert len(values) >= num_different_values


def test_lookup():
    query = {
        "workflow": [
            {
                "id": "lookup"
            }

        ],
        "message": {
            "query_graph": {
                "nodes": {
                    "n0": {
                        "categories": [
                            "biolink:Gene"
                        ]
                    },
                    "n1": {
                        "ids": [
                            "CHEBI:45783"
                        ],
                        "categories": [
                            "biolink:SmallMolecule"
                        ]
                    }
                },
                "edges": {
                    "e01": {
                        "subject": "n0",
                        "object": "n1",
                        "predicates": [
                            "biolink:related_to"
                        ]
                    }
                }
            }
        }
    }
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) > 0
    for result in message.results:
        assert result.score is None

def test_fill_success():
    query = {
        "workflow": [
            {
                "id": "fill",
                "parameters": {
                    "allowlist": ["infores:rtx-kg2"],
                    "qedge_keys": ["e01"]
                }
            }
        ],
        "message": {
            "query_graph": {
                "nodes": {
                    "n0": {
                        "categories": [
                            "biolink:Gene"
                        ]
                    },
                    "n1": {
                        "ids": [
                            "CHEBI:45783"
                        ],
                        "categories": [
                            "biolink:ChemicalSubstance"
                        ]
                    }
                },
                "edges": {
                    "e01": {
                        "subject": "n0",
                        "object": "n1",
                        "predicates": [
                            "biolink:related_to"
                        ]
                    }
                }
            }
        }
    }
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.knowledge_graph.nodes) > 0
    assert len(message.knowledge_graph.edges) > 0

def test_fill_error():
    query = {
        "workflow": [
            {
                "id": "fill",
                "parameters": {
                    "allowlist": ["infores:rtx-kg2"],
                    "qedge_keys": ["asdf"]
                }
            }
        ],
        "message": {
            "query_graph": {
                "nodes": {
                    "n0": {
                        "categories": [
                            "biolink:Gene"
                        ]
                    },
                    "n1": {
                        "ids": [
                            "CHEBI:45783"
                        ],
                        "categories": [
                            "biolink:ChemicalSubstance"
                        ]
                    }
                },
                "edges": {
                    "e01": {
                        "subject": "n0",
                        "object": "n1",
                        "predicates": [
                            "biolink:related_to"
                        ]
                    }
                }
            }
        }
    }
    [response, message] = _do_arax_query(query)
    assert response.status == 'ERROR'
    assert len(message.knowledge_graph.nodes) == 0
    assert len(message.knowledge_graph.edges) == 0

def test_score():
    query = {
        "workflow": [
            {
                "id": "lookup"
            },
            {
                "id": "score"
            }

        ],
        "message": {
            "query_graph": {
                "nodes": {
                    "n0": {
                        "categories": [
                            "biolink:Gene"
                        ]
                    },
                    "n1": {
                        "ids": [
                            "CHEBI:45783"
                        ],
                        "categories": [
                            "biolink:SmallMolecule"
                        ]
                    }
                },
                "edges": {
                    "e01": {
                        "subject": "n0",
                        "object": "n1",
                        "predicates": [
                            "biolink:related_to"
                        ]
                    }
                }
            }
        }
    }
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) > 0
    for result in message.results:
        assert result.score is not None

def test_bind():
    query = {
        "workflow": [
            {
                "id": "fill",
                "parameters": {
                    "allowlist": ["infores:rtx-kg2"]
                }
            },
            {
                "id": "bind"
            }

        ],
        "message": {
            "query_graph": {
                "nodes": {
                    "n0": {
                        "categories": [
                            "biolink:Gene"
                        ]
                    },
                    "n1": {
                        "ids": [
                            "CHEBI:45783"
                        ],
                        "categories": [
                            "biolink:ChemicalSubstance"
                        ]
                    }
                },
                "edges": {
                    "e01": {
                        "subject": "n0",
                        "object": "n1",
                        "predicates": [
                            "biolink:related_to"
                        ]
                    }
                }
            }
        }
    }
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) > 0

def test_complete_results():
    query = {
        "workflow": [
            {
                "id": "fill",
                "parameters": {
                    "allowlist": ["infores:rtx-kg2"]
                }
            },
            {
                "id": "complete_results"
            }

        ],
        "message": {
            "query_graph": {
                "nodes": {
                    "n0": {
                        "categories": [
                            "biolink:Gene"
                        ]
                    },
                    "n1": {
                        "ids": [
                            "CHEBI:45783"
                        ],
                        "categories": [
                            "biolink:ChemicalSubstance"
                        ]
                    }
                },
                "edges": {
                    "e01": {
                        "subject": "n0",
                        "object": "n1",
                        "predicates": [
                            "biolink:related_to"
                        ]
                    }
                }
            }
        }
    }
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) > 0

def test_filter_results_top_n():
    query = {
        "workflow": [
            {
                "id": "fill",
                "parameters": {
                    "allowlist": ["infores:rtx-kg2"]
                }
            },
            {
                "id": "overlay_compute_ngd",
                "parameters": {
                    "virtual_relation_label": "NGD1",
                    "qnode_keys": ["n0", "n1"]
                }
            },
            {
                "id": "bind"
            },
            {
                "id": "score"
            },
            {
                "id": "filter_results_top_n",
                "parameters": {
                    "max_results": 20
                }
            }
        ],
        "message": {
            "query_graph": {
                "nodes": {
                    "n0": {
                        "categories": [
                            "biolink:Gene"
                        ]
                    },
                    "n1": {
                        "ids": [
                            "CHEBI:45783"
                        ],
                        "categories": [
                            "biolink:SmallMolecule"
                        ]
                    }
                },
                "edges": {
                    "e01": {
                        "subject": "n0",
                        "object": "n1",
                        "predicates": [
                            "biolink:related_to"
                        ]
                    }
                }
            }
        }
    }
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) == 20
    for result in message.results:
        assert result.score is not None

def test_overlay_after_lookup():
    query = {
        "workflow": [
            {
                "id": "lookup"
            },
            {
                "id": "overlay_compute_ngd",
                "parameters": {
                    "virtual_relation_label": "NGD1",
                    "qnode_keys": ["n0", "n1"]
                }
            },
            {
                "id": "score"
            },
            {
                "id": "filter_results_top_n",
                "parameters": {
                    "max_results": 20
                }
            }
        ],
        "message": {
            "query_graph": {
                "nodes": {
                    "n0": {
                        "categories": [
                            "biolink:Gene"
                        ]
                    },
                    "n1": {
                        "ids": [
                            "CHEBI:45783"
                        ],
                        "categories": [
                            "biolink:SmallMolecule"
                        ]
                    }
                },
                "edges": {
                    "e01": {
                        "subject": "n0",
                        "object": "n1",
                        "predicates": [
                            "biolink:related_to"
                        ]
                    }
                }
            }
        }
    }
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) == 20
    ngd_bindings = set()
    for result in message.results:
        assert result.score is not None
        for eb_key, edge_bindings in result.edge_bindings.items():
            for edge_binding in edge_bindings:
                if edge_binding.id.startswith("NGD1"):
                    ngd_bindings.add(edge_binding.id)
    assert len(ngd_bindings) == len(message.results)

@pytest.mark.slow
def test_connect_knodes_2_nodes():
    query = {
      "workflow": [
        {
          "id": "fill"
        },
        {
          "id": "overlay_connect_knodes"
        },
        {
          "id": "complete_results"
        },
        {
          "id": "score"
        },
        {
          "id": "sort_results_score",
          "parameters": {
            "ascending_or_descending": "descending"
          }
        },
        {
          "id": "filter_results_top_n",
          "parameters": {
            "max_results": 30
          }
        }
      ],
      "message": {
        "query_graph": {
                "nodes": {
                    "n0": {
                        "categories": [
                            "biolink:Disease"
                        ]
                    },
                    "n1": {
                        "ids": [
                            "CHEBI:45783"
                        ],
                        "categories": [
                            "biolink:ChemicalEntity"
                        ]
                    }
                },
                "edges": {
                    "e01": {
                        "subject": "n0",
                        "object": "n1",
                        "predicates": [
                            "biolink:treats"
                        ]
                    }
                }
            }
      }
    }
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) == 30
    connected_bindings_ngd = set()
    connected_bindings_fisher = set()
    connected_bindings_paired_freq = set()
    connected_bindings_pred_dtd = set()
    for result in message.results:
        assert result.score is not None
        for eb_key, edge_bindings in result.edge_bindings.items():
            for edge_binding in edge_bindings:
                if edge_binding.id.startswith("connect_knodes_fisher"):
                    connected_bindings_fisher.add(edge_binding.id)
                elif edge_binding.id.startswith("connect_knodes_ngd"):
                    connected_bindings_ngd.add(edge_binding.id)
                elif edge_binding.id.startswith("connect_knodes_paired_freq"):
                    connected_bindings_paired_freq.add(edge_binding.id)
                elif edge_binding.id.startswith("connect_knodes_pred_dtd"):
                    connected_bindings_pred_dtd.add(edge_binding.id)
                    
    assert len(connected_bindings_ngd) > 0
    assert len(connected_bindings_fisher) > 0
    assert len(connected_bindings_paired_freq) > 0
    assert len(connected_bindings_pred_dtd) > 0

@pytest.mark.slow
def test_connect_knodes_3_nodes():
    query = {
      "workflow": [
        {
          "id": "fill"
        },
        {
          "id": "overlay_connect_knodes"
        },
        {
          "id": "complete_results"
        },
        {
          "id": "score"
        },
        {
          "id": "sort_results_score",
          "parameters": {
            "ascending_or_descending": "descending"
          }
        },
        {
          "id": "filter_results_top_n",
          "parameters": {
            "max_results": 30
          }
        }
      ],
      "message": {
        "query_graph": {
                "nodes": {
                    "n0": {
                        "categories": [
                            "biolink:Gene"
                        ]
                    },
                    "n1": {
                        "ids": [
                            "CHEBI:45783"
                        ],
                        "categories": [
                            "biolink:ChemicalEntity"
                        ]
                    },
                    "n2": {
                        "categories": [
                            "biolink:Disease"
                        ]
                    }
                },
                "edges": {
                    "e01": {
                        "subject": "n1",
                        "object": "n0",
                        "predicates": [
                            "biolink:related_to"
                        ]
                    },
                    "e02": {
                        "subject": "n0",
                        "object": "n2",
                        "predicates": [
                            "biolink:related_to"
                        ]
                    }
                }
            }
      }
    }
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) == 30
    connected_bindings = set()
    for result in message.results:
        assert result.score is not None
        for eb_key, edge_bindings in result.edge_bindings.items():
            for edge_binding in edge_bindings:
                if edge_binding.id.startswith("connect_knodes"):
                    connected_bindings.add(edge_binding.id)
    assert len(connected_bindings) > 0


def test_unknown_operation():
    # Tests if unknown operations are handled correctly
        query = {
            "workflow": [
                {
                    "id": "gobbledegook"
                },
                {
                    "id": "overlay_compute_ngd",
                    "parameters": {
                        "virtual_relation_label": "NGD1",
                        "qnode_keys": ["n0", "n1"]
                    }
                },
                {
                    "id": "score"
                },
                {
                    "id": "filter_results_top_n",
                    "parameters": {
                        "max_results": 20
                    }
                }
            ],
            "message": {
                "query_graph": {
                    "nodes": {
                        "n0": {
                            "categories": [
                                "biolink:Gene"
                            ]
                        },
                        "n1": {
                            "ids": [
                                "CHEBI:45783"
                            ],
                            "categories": [
                                "biolink:SmallMolecule"
                            ]
                        }
                    },
                    "edges": {
                        "e01": {
                            "subject": "n0",
                            "object": "n1",
                            "predicates": [
                                "biolink:related_to"
                            ]
                        }
                    }
                }
            }
        }
        [response, message] = _do_arax_query(query)
        assert response.status == 'ERROR'


if __name__ == "__main__":
    pytest.main(['-v'])


# code/ARAX/test/test_ARAX_workflows.py

#!/usr/bin/env python3

# Intended to test our more complicated workflows

import sys
import os
import pytest
from collections import Counter
import copy
import json
import ast
from typing import List, Union

import numpy as np

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../ARAXQuery")
sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../ARAXQuery")
from ARAX_query import ARAXQuery
from ARAX_response import ARAXResponse

PACKAGE_PARENT = '../../UI/OpenAPI/openapi_server'
sys.path.append(os.path.normpath(os.path.join(os.getcwd(), PACKAGE_PARENT)))
from openapi_server.models.message import Message


def _do_arax_query(query: dict) -> List[Union[ARAXResponse, Message]]:
    araxq = ARAXQuery()
    response = araxq.query(query)
    if response.status != 'OK':
        print(response.show(level=response.DEBUG))
    return [response, response.envelope.message]


def _attribute_tester(message, attribute_name: str, attribute_type: str, num_different_values=2):
    """
    Tests attributes of a message
    message: returned from _do_arax_query
    attribute_name: the attribute name to test (eg. 'jaccard_index')
    attribute_type: the attribute type (eg. 'EDAM:data_1234')
    num_different_values: the number of distinct values you wish to see have been added as attributes
    """
    edges_of_interest = []
    values = set()
    for key, edge in message.knowledge_graph.edges.items():
        if hasattr(edge, 'edge_attributes'):
            for attr in edge.edge_attributes:
                if attr.original_attribute_name == attribute_name:
                    edges_of_interest.append(edge)
                    assert attr.attribute_type_id == attribute_type
                    values.add(attr.value)
    assert len(edges_of_interest) > 0
    assert len(values) >= num_different_values


def _virtual_tester(message: Message, edge_predicate: str, relation: str, attribute_name: str, attribute_type: str, num_different_values=2):
    """
    Tests overlay functions that add virtual edges
    message: returned from _do_arax_query
    edge_predicate: the name of the virtual edge (eg. biolink:has_jaccard_index_with)
    relation: the relation you picked for the virtual_edge_relation (eg. N1)
    attribute_name: the attribute name to test (eg. 'jaccard_index')
    attribute_type: the attribute type (eg. 'EDAM:data_1234')
    num_different_values: the number of distinct values you wish to see have been added as attributes
    """
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert edge_predicate in edge_predicates_in_kg
    edges_of_interest = [x for x in message.knowledge_graph.edges.values() if x.relation == relation]
    values = set()
    assert len(edges_of_interest) > 0
    for edge in edges_of_interest:
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        assert edge.attributes[0].original_attribute_name == attribute_name
        values.add(edge.attributes[0].value)
        assert edge.attributes[0].attribute_type_id == attribute_type
    # make sure two or more values were added
    assert len(values) >= num_different_values


def test_option_group_id():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:3312, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n00, object=n01, predicates=biolink:indicated_for, option_group_key=a, id=e00)",
            "add_qedge(subject=n00, object=n01, predicates=biolink:contraindicated_for, option_group_key=1, id=e01)",
            "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
        ]}}
    [response, message] = _do_arax_query(query)
    for key, edge in message.query_graph.edges.items():
        if key == 'e01':
            assert edge.option_group_id == '1'
        elif key == 'e00':
            assert edge.option_group_id == 'a'

def test_exclude():
    query = {"operations": {"actions": [
            "create_message",
            "add_qnode(name=DOID:3312, key=n00)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n00, object=n01, predicates=biolink:treats, key=e00)",
            "add_qedge(subject=n00, object=n01, predicates=biolink:contraindicated_for, exclude=true, key=e01)",
            "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
        ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    for key, edge in message.query_graph.edges.items():
        if key == 'e01':
            assert edge.exclude
        if key == 'e00':
            assert not edge.exclude

@pytest.mark.slow
def test_example_2():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:7551, key=n00)",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:physically_interacts_with)",
        "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
        "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=jaccard_index, direction=below, threshold=.2, remove_connected_nodes=t, qnode_keys=[n02])",
        "filter_kg(action=remove_edges_by_discrete_attribute,edge_attribute=provided_by, value=Pharos)",
        "overlay(action=predict_drug_treats_disease, subject_qnode_key=n02, object_qnode_key=n00, virtual_relation_label=P1, threshold=0)",
        "resultify(ignore_edge_direction=true)",
        "filter_results(action=sort_by_edge_attribute, edge_attribute=jaccard_index, direction=descending, max_results=15)",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) > 0  # :BUG: sometimes the workflow returns 47 results, sometimes 48 (!?)
    assert message.results[0].essence is not None
    _virtual_tester(message, 'biolink:probably_treats', 'P1', 'probability_treats', 'EDAM:data_0951', 2)
    _virtual_tester(message, 'biolink:has_jaccard_index_with', 'J1', 'jaccard_index', 'EDAM:data_1772', 2)


@pytest.mark.slow
def test_example_3():
    query = {"operations": {"actions": [
        "add_qnode(name=MONDO:0005301, key=n00)", # CM: change "DOID:9406" to "MONDO:0005301" because DOID:9406 has no matched OMOP id.
        "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n01)",
        "add_qnode(categories=biolink:Protein, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01)",
        "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
        "overlay(action=overlay_clinical_info, observed_expected_ratio=true, virtual_relation_label=C1, subject_qnode_key=n00, object_qnode_key=n01)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=observed_expected_ratio, direction=below, threshold=1, remove_connected_nodes=t, qnode_keys=[n01])",
        "filter_kg(action=remove_orphaned_nodes, node_category=biolink:Protein)",
        "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n01, object_qnode_key=n02)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=normalized_google_distance, direction=above, threshold=0.85, remove_connected_nodes=t, qnode_keys=[n02])",
        "resultify(ignore_edge_direction=true, debug=true)",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    #assert len(message.results) in [47, 48]  # :BUG: sometimes the workflow returns 47 results, sometimes 48 (!?)
    assert len(message.results) >= 60
    assert message.results[0].essence is not None
    _virtual_tester(message, 'biolink:has_observed_expected_ratio_with', 'C1', 'observed_expected_ratio', 'EDAM:data_0951', 2)
    _virtual_tester(message, 'biolink:has_normalized_google_distance_with', 'N1', 'normalized_google_distance', 'EDAM:data_2526', 2)


@pytest.mark.slow
def test_FET_example_1():
    # This a FET 3-top example: try to find the phenotypes of drugs connected to proteins connected to DOID:14330
    query = {"operations": {"actions": [
        "add_qnode(ids=DOID:12889, key=n00, categories=biolink:Disease)",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(edge_key=e00, kp=infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n00, object_qnode_key=n01, virtual_relation_label=FET1, rel_edge_key=e00)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.005, remove_connected_nodes=t, qnode_keys=[n01])",
        "add_qnode(categories=biolink:ChemicalEntity, is_set=true, key=n02)",
        "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:physically_interacts_with)",
        "expand(edge_key=e01, kp=infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n01, object_qnode_key=n02, virtual_relation_label=FET2, rel_edge_key=e01)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.005, remove_connected_nodes=t, qnode_keys=[n02])",
        "add_qnode(categories=biolink:PhenotypicFeature, key=n03)",
        "add_qedge(subject=n02, object=n03, key=e02)",
        "expand(edge_key=e02, kp=infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n02, object_qnode_key=n03, virtual_relation_label=FET3, rel_edge_key=e02)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.005, remove_connected_nodes=t, qnode_keys=[n03])",
        "resultify()",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert message.n_results > 0
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert 'biolink:has_fisher_exact_test_p_value_with' in edge_predicates_in_kg
    FET_edges = [x for x in message.knowledge_graph.edges.values() if x.relation is not None and x.relation.find("FET") != -1]
    FET_edge_labels = set([edge.relation for edge in FET_edges])
    assert len(FET_edge_labels) == 3
    for edge in FET_edges:
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        edge_attributes_dict = {attr.original_attribute_name:attr.value for attr in edge.attributes}
        assert edge.attributes[0].original_attribute_name == 'fisher_exact_test_p-value'
        assert 0 <= float(edge.attributes[0].value) < 0.005
        assert edge.attributes[0].attribute_type_id == 'EDAM:data_1669'
        #assert edge_attributes_dict['is_defined_by'] == 'ARAX'
        assert edge_attributes_dict['provided_by'] == 'ARAX'
    FET_query_edges = {key:edge for key, edge in message.query_graph.edges.items() if key.find("FET") != -1}
    assert len(FET_query_edges) == 3
    query_node_keys = [key for key, node in message.query_graph.nodes.items()]
    assert len(query_node_keys) == 4
    for key, query_edge in FET_query_edges.items():
        assert hasattr(query_edge, 'predicates')
        assert 'biolink:has_fisher_exact_test_p_value_with' in query_edge.predicates
        assert key == query_edge.relation
        assert query_edge.subject in query_node_keys
        assert query_edge.object in query_node_keys


@pytest.mark.slow
@pytest.mark.skip(reason="FW: This now runs increadibly slowly and uses a lot of ram after changing to RTX-KG2. Need to find a better drug with fewer connections")
def test_FET_example_2():
    # This a FET 4-top example: try to find the diseases connected to proteins connected to biological_process connected to protein connected to CHEMBL.COMPOUND:CHEMBL521
    query = {"operations": {"actions": [
        "add_qnode(key=n00, ids=CHEMBL.COMPOUND:CHEMBL1472, categories=biolink:ChemicalEntity)",
        "add_qnode(key=n01, is_set=true, categories=biolink:Protein)",
        "add_qedge(key=e00, subject=n00, object=n01)",
        "expand(edge_key=e00, infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n00, object_qnode_key=n01, virtual_relation_label=FET1)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.01, remove_connected_nodes=t, qnode_keys=[n01])",
        "add_qnode(categories=biolink:BiologicalProcess, is_set=true, key=n02)",
        "add_qedge(subject=n01, object=n02, key=e01)",
        "expand(edge_key=e01, infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n01, object_qnode_key=n02, virtual_relation_label=FET2)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.01, remove_connected_nodes=t, qnode_keys=[n02])",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n03)",
        "add_qedge(subject=n02, object=n03, key=e02)",
        "expand(edge_key=e02, infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n02, object_qnode_key=n03, virtual_relation_label=FET3)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.01, remove_connected_nodes=t, qnode_keys=[n03])",
        "add_qnode(categories=biolink:Disease, key=n04)",
        "add_qedge(subject=n03, object=n04, key=e03)",
        "expand(edge_key=e03, infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n03, object_qnode_key=n04, virtual_relation_label=FET4)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.01, remove_connected_nodes=t, qnode_keys=[n04])",
        "resultify()",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert message.n_results > 0
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert 'biolink:has_fisher_exact_test_p_value_with' in edge_predicates_in_kg
    FET_edges = [x for x in message.knowledge_graph.edges.values() if x.relation is not None and x.relation.find("FET") != -1]
    FET_edge_labels = set([edge.relation for edge in FET_edges])
    assert len(FET_edge_labels) == 4
    for edge in FET_edges:
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        edge_attributes_dict = {attr.original_attribute_name:attr.value for attr in edge.attributes}
        assert edge.attributes[0].original_attribute_name == 'fisher_exact_test_p-value'
        assert 0 <= float(edge.attributes[0].value) < 0.01
        assert edge.attributes[0].attribute_type_id == 'EDAM:data_1669'
        #assert edge_attributes_dict['is_defined_by'] == 'ARAX'
        assert edge_attributes_dict['provided_by'] == 'ARAX'
    FET_query_edges = {key:edge for key, edge in message.query_graph.edges.items() if key.find("FET") != -1}
    assert len(FET_query_edges) == 4
    query_node_keys = [key for key, node in message.query_graph.nodes.items()]
    assert len(query_node_keys) == 5
    for key, query_edge in FET_query_edges.items():
        assert hasattr(query_edge, 'predicates')
        assert 'biolink:has_fisher_exact_test_p_value_with' in query_edge.predicates
        assert key == query_edge.relation
        assert query_edge.subject in query_node_keys
        assert query_edge.object in query_node_keys


@pytest.mark.skip(reason="need issue#846 to be solved")
def test_FET_example_3():
    # This a FET 6-top example: try to find the drugs connected to proteins connected to pathways connected to proteins connected to diseases connected to phenotypes of DOID:14330
    query = {"operations": {"actions": [
        "add_qnode(ids=DOID:14330, key=n00, categories=biolink:Disease)",
        "add_qnode(categories=biolink:PhenotypicFeature, is_set=true, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00, predicates=biolink:has_phenotype)",
        "expand(edge_key=e00, infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n00, object_qnode_key=n01, virtual_relation_label=FET1, rel_edge_key=e00)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.001, remove_connected_nodes=t, qnode_keys=[n01])",
        "add_qnode(categories=biolink:Disease, is_set=true, key=n02)",
        "add_qedge(subject=n01,object=n02,key=e01,predicates=biolink:has_phenotype)",
        "expand(edge_key=e01,infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n01, object_qnode_key=n02, virtual_relation_label=FET2, rel_edge_key=e01)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.001, remove_connected_nodes=t, qnode_keys=[n02])",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n03)",
        "add_qedge(subject=n02, object=n03, key=e02, predicates=biolink:gene_mutations_contribute_to)",
        "expand(edge_key=e02, infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n02, object_qnode_key=n03, virtual_relation_label=FET3, rel_edge_key=e02)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.001, remove_connected_nodes=t, qnode_keys=[n03])",
        "add_qnode(categories=biolink:Pathway, is_set=true, key=n04)",
        "add_qedge(subject=n03, object=n04, key=e03, predicates=biolink:participates_in)",
        "expand(edge_key=e03, infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n03, object_qnode_key=n04, virtual_relation_label=FET4, rel_edge_key=e03)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.001, remove_connected_nodes=t, qnode_keys=[n04])",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n05)",
        "add_qedge(subject=n04, object=n05, key=e04, predicates=biolink:participates_in)",
        "expand(edge_key=e04, infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n04, object_qnode_key=n05, virtual_relation_label=FET5, rel_edge_key=e04)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.001, remove_connected_nodes=t, qnode_keys=[n05])",
        "add_qnode(categories=biolink:ChemicalEntity, key=n06)",
        "add_qedge(subject=n05, object=n06, key=e05, predicates=biolink:physically_interacts_with)",
        "expand(edge_key=e05, infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n05, object_qnode_key=n06, virtual_relation_label=FET6, rel_edge_key=e05)",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=fisher_exact_test_p-value, direction=above, threshold=0.001, remove_connected_nodes=t, qnode_keys=[n06])",
        "resultify()",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert message.n_results > 0
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert 'biolink:has_fisher_exact_test_p_value_with' in edge_predicates_in_kg
    FET_edges = [x for x in message.knowledge_graph.edges.values() if x.relation.find("FET") != -1]
    FET_edge_labels = set([edge.relation for edge in FET_edges])
    assert len(FET_edge_labels) == 6
    for edge in FET_edges:
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        edge_attributes_dict = {attr.original_attribute_name:attr.value for attr in edge.attributes}
        assert edge.attributes[0].original_attribute_name == 'fisher_exact_test_p-value'
        assert 0 <= float(edge.attributes[0].value) < 0.001
        assert edge.attributes[0].attribute_type_id == 'EDAM:data_1669'
        #assert edge_attributes_dict['is_defined_by'] == 'ARAX'
        assert edge_attributes_dict['provided_by'] == 'ARAX'
    FET_query_edges = {key:edge for key, edge in message.query_graph.edges.items() if key.find("FET") != -1}
    assert len(FET_query_edges) == 4
    query_node_keys = [key for key, node in message.query_graph.nodes.items()]
    assert len(query_node_keys) == 5
    for key, query_edge in FET_query_edges.items():
        assert hasattr(query_edge, 'predicates')
        assert 'biolink:has_fisher_exact_test_p_value_with' in query_edge.predicates
        assert key == query_edge.relation
        assert query_edge.subject in query_node_keys
        assert query_edge.object in query_node_keys


@pytest.mark.slow
def test_FET_example_4():
    # This a FET 2-top example collecting nodes and edges from both KG1 and KG2: try to find the disease connected to proteins connected to DOID:14330
    # FW: Now only does KG2 since we are disabling KG1
    query = {"operations": {"actions": [
        "add_qnode(ids=DOID:10718, key=n00, categories=biolink:Disease)",
        "add_qnode(categories=biolink:PhenotypicFeature, is_set=true, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(edge_key=e00, infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n00, virtual_relation_label=FET1, object_qnode_key=n01,rel_edge_key=e00)",
        "filter_kg(action=remove_edges_by_continuous_attribute,edge_attribute=fisher_exact_test_p-value,direction=above,threshold=0.001,remove_connected_nodes=t,qnode_keys=[n01])",
        "add_qnode(categories=biolink:Disease, key=n02)",
        "add_qedge(subject=n01, object=n02, key=e01)",
        "expand(edge_key=e01, infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n01, virtual_relation_label=FET2, object_qnode_key=n02,rel_edge_id=e01)",
        "resultify()",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert message.n_results > 0
    edge_predicates_in_kg = Counter([x.predicate for x in message.knowledge_graph.edges.values()])
    assert 'biolink:has_fisher_exact_test_p_value_with' in edge_predicates_in_kg
    FET_edges = [x for x in message.knowledge_graph.edges.values() if x.relation and x.relation.find("FET") != -1]
    FET_edge_labels = set([edge.relation for edge in FET_edges])
    assert len(FET_edge_labels) == 2
    for edge in FET_edges:
        assert hasattr(edge, 'attributes')
        assert edge.attributes
        edge_attributes_dict = {attr.original_attribute_name:attr.value for attr in edge.attributes}
        assert edge.attributes[0].original_attribute_name == 'fisher_exact_test_p-value'
        if edge.relation == "FET1":
            assert 0 <= float(edge.attributes[0].value) < 0.001
        else:
            assert float(edge.attributes[0].value) >= 0
        assert edge.attributes[0].attribute_type_id == 'EDAM:data_1669'
        #assert edge_attributes_dict['is_defined_by'] == 'ARAX'
        assert edge_attributes_dict['provided_by'] == 'ARAX'
    FET_query_edges = {key:edge for key, edge in message.query_graph.edges.items() if key.find("FET") != -1}
    assert len(FET_query_edges) == 2
    query_node_keys = [key for key, node in message.query_graph.nodes.items()]
    assert len(query_node_keys) == 3
    for key, query_edge in FET_query_edges.items():
        assert hasattr(query_edge, 'predicates')
        assert 'biolink:has_fisher_exact_test_p_value_with' in query_edge.predicates
        assert key == query_edge.relation
        assert query_edge.subject in query_node_keys
        assert query_edge.object in query_node_keys

@pytest.mark.slow
def test_FET_ranking_1():
    query = {"operations": { "actions": [
            "create_message",
            "add_qnode(key=n00,ids=UniProtKB:P14136,categories=biolink:Protein)",
            "add_qnode(categories=biolink:BiologicalProcess, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00,kp=infores:rtx-kg2)",
            "overlay(action=fisher_exact_test, subject_qnode_key=n00, object_qnode_key=n01, virtual_relation_label=FET)",
            "resultify()",
            "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    ranks = [result.score for result in message.results]
    assert min(ranks) != max(ranks)

@pytest.mark.slow
def test_example_2_kg2():
    query = {"operations": { "actions": [
            "create_message",
            "add_qnode(name=DOID:14330, key=n00)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:molecularly_interacts_with)",
            "expand(edge_key=[e00,e01], infores:rtx-kg2)",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",  # seems to work just fine
            "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=jaccard_index, direction=below, threshold=.008, remove_connected_nodes=t, qnode_keys=[n02])",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=sort_by_edge_attribute, edge_attribute=jaccard_index, direction=descending, max_results=15)",
            "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert len(message.results) == 15
    assert message.results[0].essence is not None
    _virtual_tester(response.envelope.message, 'biolink:has_jaccard_index_with', 'J1', 'jaccard_index', 'EDAM:data_1772', 2)


@pytest.mark.slow
def test_clinical_overlay_example1():
    """
    Gives an example of a KG that does not have edges that COHD can decorate, but does have pairs of nodes that COHD
    could decorate (eg here is drug and chemical_substance), so add the info in as a virtual edge.
    """
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:11830, key=n00)",
        "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:molecularly_interacts_with)",
        "expand(edge_key=[e00,e01], infores:rtx-kg2)",
        # overlay a bunch of clinical info
        "overlay(action=overlay_clinical_info, paired_concept_frequency=true, subject_qnode_key=n00, object_qnode_key=n02, virtual_relation_label=C1)",
        "overlay(action=overlay_clinical_info, observed_expected_ratio=true, subject_qnode_key=n00, object_qnode_key=n02, virtual_relation_label=C2)",
        "overlay(action=overlay_clinical_info, chi_square=true, subject_qnode_key=n00, object_qnode_key=n02, virtual_relation_label=C3)",
        # filter some stuff out for the fun of it
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=paired_concept_frequency, direction=above, threshold=0.5, remove_connected_nodes=true, qnode_keys=[n02])",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=observed_expected_ratio, direction=above, threshold=1, remove_connected_nodes=true, qnode_keys=[n02])",
        "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=chi_square, direction=below, threshold=0.05, remove_connected_nodes=true, qnode_keys=[n02])",
        # return results
        "resultify(ignore_edge_direction=true)",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    _virtual_tester(message, 'biolink:has_paired_concept_frequency_with', 'C1', 'paired_concept_frequency', 'EDAM:data_0951', 2)
    _virtual_tester(message, 'biolink:has_observed_expected_ratio_with', 'C2', 'observed_expected_ratio', 'EDAM:data_0951', 2)
    _virtual_tester(message, 'biolink:has_chi_square_with', 'C3', 'chi_square', 'EDAM:data_0951', 2)


@pytest.mark.skip(reason="redundant if the test_clinical_overlay_example() passes and test_ARAX_overlay passes")
def test_clinical_overlay_example2():
    """
    Gives an example of overlaying (and filtering) clinical attributes when there exist edges in the KG that COHD can decorate
    """
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=DOID:11830, key=n00)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(edge_key=e00, infores:rtx-kg2)",
        # overlay a bunch of clinical info
        "overlay(action=overlay_clinical_info, paired_concept_frequency=true)",
        "overlay(action=overlay_clinical_info, observed_expected_ratio=true)",
        "overlay(action=overlay_clinical_info, chi_square=true)",
        # filter some stuff out for the fun of it
        "filter_kg(action=remove_edges_by_attribute_default, edge_attribute=paired_concept_frequency, type=std, remove_connected_nodes=F)",
        "filter_kg(action=remove_edges_by_attribute_default, edge_attribute=observed_expected_ratio, type=std, remove_connected_nodes=F)",
        "filter_kg(action=remove_edges_by_attribute_default, edge_attribute=chi_square, type=std, remove_connected_nodes=F)",
        # return results
        "resultify(ignore_edge_direction=true)",
        "return(message=true, store=false)",
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    _attribute_tester(message, 'paired_concept_frequency', 'EDAM:data_0951', 1)
    _attribute_tester(message, 'observed_expected_ratio', 'EDAM:data_0951', 1)
    _attribute_tester(message, 'chi_square', 'EDAM:data_0951', 1)


@pytest.mark.skip(reason="redundant if test_one_hop_based_on_types_1() and test_ARAX_overlay() passes")
def test_two_hop_based_on_types_1():
    """
    Example DSL for a two hop question that is based on types
    """
    #doid_list = {"DOID:11830", "DOID:5612", "DOID:2411", "DOID:8501", "DOID:174"}
    doid_list = {"DOID:11830"}
    for doid in doid_list:
        query = {"operations": {"actions": [
            "create_message",
            f"add_qnode(name={doid}, key=n00, categories=biolink:Disease)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01)",
            "expand(edge_key=e00, infores:rtx-kg2)",
            #"expand(edge_key=e00, kp=infores:biothings-explorer)",
            "expand(edge_key=e01, infores:rtx-kg2)",
            "overlay(action=overlay_clinical_info, paired_concept_frequency=true, subject_qnode_key=n00, object_qnode_key=n02, virtual_relation_label=C1)",
            "overlay(action=overlay_clinical_info, observed_expected_ratio=true, subject_qnode_key=n00, object_qnode_key=n02, virtual_relation_label=C2)",
            "overlay(action=overlay_clinical_info, chi_square=true, subject_qnode_key=n00, object_qnode_key=n02, virtual_relation_label=C3)",
            "overlay(action=predict_drug_treats_disease, subject_qnode_key=n02, object_qnode_key=n00, virtual_relation_label=P1)",
            "overlay(action=compute_ngd)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=limit_number_of_results, max_results=50)",
            "return(message=false, store=true)",
        ]}}
        [response, message] = _do_arax_query(query)
        print(message.id)
        assert response.status == 'OK'
        _virtual_tester(message, 'biolink:has_paired_concept_frequency_with', 'C1', 'paired_concept_frequency', 'EDAM:data_0951', 1)
        _virtual_tester(message, 'biolink:has_observed_expected_ratio_with', 'C2', 'observed_expected_ratio', 'EDAM:data_0951', 1)
        _virtual_tester(message, 'biolink:has_chi_square_with', 'C3', 'chi_square', 'EDAM:data_0951', 1)
        assert len(message.results) > 1


@pytest.mark.external
@pytest.mark.slow
def test_one_hop_based_on_types_1():
    """
    Example DSL for a one hop question that is based on types
    """
    #doid_list = {"DOID:11830", "DOID:5612", "DOID:2411", "DOID:8501", "DOID:174"}
    doid_list = {"DOID:11830"}
    for doid in doid_list:
        query = {"operations": {"actions": [
            "create_message",
            f"add_qnode(ids={doid}, key=n00, categories=biolink:Disease)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n01)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "expand(edge_key=e00, infores:rtx-kg2)",
            "expand(edge_key=e00, kp=infores:biothings-explorer)",
            "overlay(action=overlay_clinical_info, observed_expected_ratio=true)",
            "overlay(action=predict_drug_treats_disease)",
            "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=probability_treats, direction=below, threshold=0.75, remove_connected_nodes=true, qnode_keys=[n01])",
            "overlay(action=compute_ngd)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=limit_number_of_results, max_results=50)",
            "return(message=true, store=false)",
        ]}}
        [response, message] = _do_arax_query(query)
        assert response.status == 'OK'
        assert len(message.results) > 1


@pytest.mark.skip(reason="Work in progress (and takes a very long time)")
def test_one_hop_kitchen_sink_BTE_1():
    """
    Example of throwing everything at a simple BTE query
    """
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(curie=DOID:11830, key=n0, categories=biolink:Disease)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e1)",
        #"expand(edge_key=e00, infores:rtx-kg2)",
        "expand(edge_key=e1, kp=infores:biothings-explorer)",
        "overlay(action=overlay_clinical_info, paired_concept_frequency=true)",
        "overlay(action=overlay_clinical_info, observed_expected_ratio=true)",
        "overlay(action=overlay_clinical_info, chi_square=true)",
        "overlay(action=predict_drug_treats_disease)",
        "overlay(action=compute_ngd)",
        "resultify(ignore_edge_direction=true)",
        "filter_results(action=limit_number_of_results, max_results=50)",
        "return(message=true, store=true)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(message.id)
    assert response.status == 'OK'
    _attribute_tester(message, 'paired_concept_frequency', 'EDAM:data_0951', 1)
    _attribute_tester(message, 'observed_expected_ratio', 'EDAM:data_0951', 1)
    _attribute_tester(message, 'chi_square', 'EDAM:data_0951', 1)


@pytest.mark.skip(reason="Work in progress (and takes a very long time)")
def test_one_hop_kitchen_sink_BTE_2():
    """
    Example of throwing everything at a simple BTE query, but with node types that aren't appropriate for some reasoning capabilities
    """
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(curie=DOID:11830, key=n0, categories=biolink:Disease)",
        "add_qnode(categories=biolink:Gene, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e1)",
        #"expand(edge_key=e00, infores:rtx-kg2)",
        "expand(edge_key=e1, kp=infores:biothings-explorer)",
        "overlay(action=overlay_clinical_info, paired_concept_frequency=true)",
        "overlay(action=overlay_clinical_info, observed_expected_ratio=true)",
        "overlay(action=overlay_clinical_info, chi_square=true)",
        "overlay(action=predict_drug_treats_disease)",
        "overlay(action=compute_ngd)",
        "resultify(ignore_edge_direction=true)",
        "filter_results(action=limit_number_of_results, max_results=50)",
        "return(message=true, store=true)",
    ]}}
    [response, message] = _do_arax_query(query)
    print(message.id)
    assert response.status == 'OK'
    _attribute_tester(message, 'paired_concept_frequency', 'EDAM:data_0951', 1)
    _attribute_tester(message, 'observed_expected_ratio', 'EDAM:data_0951', 1)
    _attribute_tester(message, 'chi_square', 'EDAM:data_0951', 1)

def test_FET_ranking_2():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(key=n00,ids=[UniProtKB:P14136,UniProtKB:P35579],is_set=true,categories=biolink:Protein)",
        "add_qnode(categories=biolink:BiologicalProcess, key=n01)",
        "add_qedge(subject=n00, object=n01, key=e00)",
        "expand(edge_key=e00,kp=infores:rtx-kg2)",
        "overlay(action=fisher_exact_test, subject_qnode_key=n00, object_qnode_key=n01, virtual_relation_label=FET)",
        "resultify()",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    fet_ranking_value = {}
    for result in message.results:
        for key, edge_bindings in result.edge_bindings.items():
            if key.startswith('FET'):
                for edge in edge_bindings:
                    for attribute in message.knowledge_graph.edges[edge.id].attributes:
                        if attribute.original_attribute_name == "fisher_exact_test_p-value":
                            if str(result.score) in fet_ranking_value:
                                fet_ranking_value[str(result.score)].append(float(attribute.value))
                            else:
                                fet_ranking_value[str(result.score)] = [float(attribute.value)]

    for fet_val, conf_list in fet_ranking_value.items():
        if len(conf_list) > 1:
            for diff in [abs(x - y) for i,x in enumerate(conf_list) for j,y in enumerate(conf_list) if i < j]:
                assert diff == 0


@pytest.mark.external
def test_genetics_kp_ranking():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(name=type 2 diabetes mellitus, categories=biolink:Disease, key=n0)",
        "add_qnode(categories=biolink:Gene, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0, predicates=biolink:condition_associated_with_gene)",
        "expand(edge_key=e0,kp=infores:genetics-data-provider)",
        "resultify()",
        "filter_results(action=limit_number_of_results, max_results=30)",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    gen_kp_ranking_value = {}
    for result in message.results:
        for key, edge_bindings in result.edge_bindings.items():
            for edge in edge_bindings:
                if edge.id.startswith('infores:genetics-data-provider'):
                    if message.knowledge_graph.edges[edge.id].attributes is not None:
                        for attribute in message.knowledge_graph.edges[edge.id].attributes:
                            if attribute.original_attribute_name == "pValue":
                                if str(result.score) in gen_kp_ranking_value:
                                    gen_kp_ranking_value[str(result.score)].append(float(attribute.value))
                                else:
                                    gen_kp_ranking_value[str(result.score)] = [float(attribute.value)]
    assert len(gen_kp_ranking_value) > 0

@pytest.mark.slow
def test_ranker_float_error_ex1():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(ids=CHEMBL.COMPOUND:CHEMBL112, key=n0, categories=biolink:ChemicalEntity)",
        "add_qnode(categories=biolink:DiseaseOrPhenotypicFeature, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand()",
        "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n0, object_qnode_key=n1)",
        "overlay(action=fisher_exact_test,subject_qnode_key=n0,virtual_relation_label=F1,object_qnode_key=n1)",
        "overlay(action=overlay_clinical_info,COHD_method=paired_concept_frequency,virtual_relation_label=C1,subject_qnode_key=n0,object_qnode_key=n1)",
        "overlay(action=predict_drug_treats_disease,virtual_relation_label=P1,subject_qnode_key=n0,object_qnode_key=n1,threshold=0.8,slow_mode=false)",
        "resultify()",
        "filter_results(action=limit_number_of_results, max_results=30)",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'

@pytest.mark.external
def test_ranker_float_error_ex2():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(ids=MONDO:0005301, key=n0)",
        "add_qnode(categories=biolink:ChemicalEntity, key=n1)",
        "add_qedge(subject=n0, object=n1, key=e0)",
        "expand(kp=infores:cohd)",
        "resultify()",
        "filter_results(action=limit_number_of_results, max_results=30)",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    result_scores = [x.score for x in message.results]
    assert max(result_scores) == 1

@pytest.mark.external
def test_cmap_ranking():
    query = {"operations": {"actions": [
        "create_message",
        "add_qnode(key=n00,categories=biolink:Gene,ids=HGNC:321)",
        "add_qnode(categories=biolink:ChemicalEntity)",
        "add_qedge(subject=n00,object=n01)",
        "expand(kp=infores:molepro)",
        "resultify()",
        "filter_results(action=limit_number_of_results, max_results=500)",
        "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    result_scores = {x.score for x in message.results}
    assert len(result_scores) > 1

@pytest.mark.slow
def test_ranker_float_error_ex3():
    query={"message": {
            "query_graph": {
              "edges": {
                "e00": {
                  "object": "n01",
                  "subject": "n00"
                },
                "e01": {
                  "object": "n02",
                  "subject": "n01"
                }
              },
              "nodes": {
                "n00": {
                  "categories": [
                    "biolink:Disease"
                  ],
                  "ids": [
                    "DOID:14330"
                  ]
                },
                "n01": {
                  "categories": [
                    "biolink:NamedThing"
                  ]
                },
                "n02": {
                  "categories": [
                    "biolink:Disease"
                  ],
                  "ids": [
                    "DOID:8778"
                  ]
                }
              }
            }
          }
        }
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'


@pytest.mark.external
def test_issue_1848():
    query = {
            "workflow": [
                {
                    "id": "fill",
                    "parameters": { "allowlist": ["infores:cohd"],
                        "qedge_keys": [
                            "e0"
                        ]
                    }
                },
                {
                    "id": "overlay_compute_ngd",
                    "parameters": {
                        "virtual_relation_label": "N1",
                        "qnode_keys": [
                            "n0",
                            "n1"
                        ]
                    }
                },
                {
                    "id": "bind"
                },
                {
                    "id": "score"
                },
                {
                    "id": "filter_results_top_n",
                    "parameters": {
                        "max_results": 3
                    }
                },
                {
                    "id": "fill",
                    "parameters": { "allowlist": ["infores:rtx-kg2"],
                        "qedge_keys": [
                            "e1",
                            "e2",
                            "e3",
                            "e4"
                        ]
                    }
                },
                {
                    "id": "bind"
                },
                {
                    "id": "score"
                }
            ],
            "message": {
                "query_graph": {
                    "edges": {
                        "e0": {
                            "subject": "n0",
                            "object": "n1",
                            "predicates": [
                                "biolink:has_real_world_evidence_of_association_with"
                            ]
                        },
                        "e1": {
                            "subject": "n1",
                            "object": "n2",
                            "predicates": [
                                "biolink:increases_activity_of"
                            ]
                        },
                        "e2": {
                            "subject": "n3",
                            "object": "n2",
                            "predicates": [
                                "biolink:increases_activity_of"
                            ]
                        },
                        "e3": {
                            "subject": "n1",
                            "object": "n2",
                            "predicates": [
                                "biolink:decreases_activity_of"
                            ],
                            "option_group_id": "decr"
                        },
                        "e4": {
                            "subject": "n3",
                            "object": "n2",
                            "predicates": [
                                "biolink:decreases_activity_of"
                            ],
                            "option_group_id": "decr"
                        }
                    },
                    "nodes": {
                        "n0": {
                            "ids": [
                                "MONDO:0009061"
                            ],
                            "name": "MONDO:0009061"
                        },
                        "n1": {
                            "categories": [
                                "biolink:ChemicalEntity"
                            ]
                        },
                        "n2": {
                            "categories": [
                                "biolink:Gene",
                                "biolink:Protein"
                            ]
                        },
                        "n3": {
                            "categories": [
                                "biolink:ChemicalEntity"
                            ]
                        }
                    }
                }
            }
        }
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'

# Not working yet
# def test_example_3_kg2():
#     query = {"operations": { "actions": [
#             "create_message",
#             #"add_qnode(key=n00, curie=DOID:0050156)",  # idiopathic pulmonary fibrosis
#             "add_qnode(curie=DOID:9406, key=n00)",  # hypopituitarism, original demo example
#             "add_qnode(key=n01, categories=chemical_substance, is_set=true)",
#             "add_qnode(key=n02, categories=protein)",
#             "add_qedge(key=e00, subject=n00, object=n01)",
#             "add_qedge(key=e01, subject=n01, object=n02)",
#             "expand(edge_key=[e00,e01], infores:rtx-kg2)",
#             "overlay(action=overlay_clinical_info, observed_expected_ratio=true, virtual_relation_label=C1, subject_qnode_key=n00, object_qnode_key=n01)",
#             "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n01, object_qnode_key=n02)",
#             "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=observed_expected_ratio, direction=below, threshold=2, remove_connected_nodes=t, qnode_keys=[n01])",
#             "filter_kg(action=remove_orphaned_nodes, node_category=protein)",
#             "return(message=true, store=false)"
#     ]}}
#     [response, message] = _do_arax_query(query)
#     assert response.status == 'OK'
#     #assert len(message.results) == ?
#     assert message.results[0].essence is not None
#     _virtual_tester(message, 'biolink:has_observed_expected_ratio_with', 'C1', 'observed_expected_ratio', 'EDAM:data_0951', 2)
#     _virtual_tester(message, 'biolink:has_normalized_google_distance_with', 'N1', 'normalized_google_distance', 'EDAM:data_2526', 2)

@pytest.mark.slow
def test_example_3_issue_679():
    query = {"operations": { "actions": [
            "create_message",
            "add_qnode(name=DOID:9406, key=n00)",
            "add_qnode(categories=[biolink:ChemicalEntity], is_set=true, key=n01)",
            "add_qnode(categories=[biolink:Protein], key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01)",
            "expand()",
            "overlay(action=overlay_clinical_info, COHD_method=observed_expected_ratio, virtual_relation_label=C1, subject_qnode_key=n00, object_qnode_key=n01)",
            "filter_kg(action=remove_edges_by_continuous_attribute,edge_attribute=observed_expected_ratio,direction=below,threshold=3,remove_connected_nodes=true,qnode_keys=n01)",
            "filter_kg(action=remove_orphaned_nodes,node_category=biolink:Protein)",
            "overlay(action=compute_ngd, virtual_relation_label=N1, subject_qnode_key=n01, object_qnode_key=n02)",
            "filter_kg(action=remove_edges_by_continuous_attribute,edge_attribute=ngd,direction=above,threshold=0.85,remove_connected_nodes=true,qnode_keys=n02)",
            "resultify(ignore_edge_direction=true, debug=true)",
            "return(message=true, store=false)"
    ]}}
    [response, message] = _do_arax_query(query)
    assert response.status == 'OK'
    assert message.results[0].essence is not None


if __name__ == "__main__":
    pytest.main(['-v'])


# code/ARAX/test_production_api/test_ARAX_api.py

#!/usr/bin/env python3

import sys
import os
import pytest

import copy
import json
import ast

import requests

arax_url = "https://arax.ncats.io/api/arax/v1.2/query"

def test_api_status():
    query = {
      "submitter": 'ARAX_test',
      "bypass_cache": False,
      "enforce_edge_directionality": False,
      "max_results": 100,
      "message": {
        "query_graph": {
          "edges": {
            "e00": {
              "object": "n01",
              "predicates": ["biolink:physically_interacts_with"],
              "subject": "n00"
            }
          },
          "nodes": {
            "n00": {
              "categories": ["biolink:ChemicalEntity"],
              "ids": ["CHEMBL.COMPOUND:CHEMBL112"]
            },
            "n01": {
              "categories": ["biolink:Protein"]
            }
          }
        }
      },
      "page_number": 1,
      "page_size": 100,
      "return_minimal_metadata": False,
      "stream_progress": False
    }
    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 200
    results_json = response_content.json()
    assert results_json['status'] == 'OK'
    assert len(results_json["message"]["results"]) > 0


@pytest.mark.slow
def test_kitchen_sink_api():
    query = {
  "submitter": 'ARAX_test',
  "bypass_cache": False,
  "enforce_edge_directionality": False,
  "max_results": 100,
  "message": {},
    "operations": {"actions": [
            "add_qnode(name=arthritis, key=n00)",
            "add_qnode(categories=biolink:Protein, is_set=true, key=n01)",
            "add_qnode(categories=biolink:ChemicalEntity, key=n02)",
            "add_qedge(subject=n00, object=n01, key=e00)",
            "add_qedge(subject=n01, object=n02, key=e01, predicates=biolink:physically_interacts_with)",
            "expand(edge_key=[e00,e01], kp=infores:rtx-kg2)",
            "overlay(action=overlay_clinical_info, observed_expected_ratio=true, virtual_relation_label=C1, subject_qnode_key=n00, object_qnode_key=n02)",
            "filter_kg(action=remove_edges_by_continuous_attribute, edge_attribute=probably_treats, direction=below, threshold=.8, remove_connected_nodes=t, qnode_keys=[n02])",
            "overlay(action=compute_jaccard, start_node_key=n00, intermediate_node_key=n01, end_node_key=n02, virtual_relation_label=J1)",
            "overlay(action=predict_drug_treats_disease, subject_qnode_key=n02, object_qnode_key=n00, virtual_relation_label=P1)",
            "resultify(ignore_edge_direction=true)",
            "filter_results(action=limit_number_of_results, max_results=15)",
            "return(message=true, store=false)"
        ]},
  "page_number": 1,
  "page_size": 100,
  "return_minimal_metadata": False,
  "stream_progress": False
}
    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 200
    results_json = response_content.json()
    message = results_json['message']
    assert results_json['status'] == 'OK'
    assert len(message['results']) > 0
    assert message['results'][0]['essence'] is not None
    return results_json


if __name__ == "__main__": pytest.main(['-v'])


# code/ARAX/test_production_api/test_ARAX_api_failure_modes.py

#!/usr/bin/env python3

import sys
import os
import pytest

import copy
import json
import ast

import requests

arax_url = "https://arax.ncats.io/api/arax/v1.2/query"


def test_non_json():
    content = '{'
    response_content = requests.post(arax_url, data=content, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 415
    response_json = response_content.json()
    print(json.dumps(response_json,sort_keys=True,indent=2))
    assert 'title' in response_json
    assert response_json['title'] == "Invalid Content-type (), expected JSON data"


def test_non_trapi():
    query = { "bananas": "giraffes"  }

    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 400
    response_json = response_content.json()
    #print(json.dumps(response_json,sort_keys=True,indent=2))
    assert 'detail' in response_json
    assert response_json['detail'] == "'message' is a required property"


def test_missing_message():
    query = {
        "query_graph": {
          "edges": {
            "e00": { "subject": "n00", "object": "n01", "predicates": ["biolink:physically_interacts_with"] }
          },
          "nodes": {
            "n00": { "categories": ["biolink:ChemicalSubstance"], "ids": ["CHEMBL.COMPOUND:CHEMBL112"] },
            "n01": { "categories": ["biolink:Protein"] }
       }  }  }

    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 400
    response_json = response_content.json()
    #print(json.dumps(response_json,sort_keys=True,indent=2))
    assert 'detail' in response_json
    assert response_json['detail'] == "'message' is a required property"


def test_missing_nodes():
    query = {
      "submitter": 'ARAX_test',
      "message": {
        "query_graph": {
          "edges": {
            "e00": { "subject": "n00", "object": "n01", "predicates": ["biolink:physically_interacts_with"] }
          }
      }  }  }

    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 400
    response_json = response_content.json()
    #print(json.dumps(response_json,sort_keys=True,indent=2))
    assert 'detail' in response_json
    assert response_json['detail'] == "'nodes' is a required property - 'message.query_graph'"


def test_empty_nodes():
    query = {
      "submitter": 'ARAX_test',
      "message": {
        "query_graph": {
          "edges": {
            "e00": { "subject": "n00", "object": "n01", "predicates": ["biolink:physically_interacts_with"] }
          },
          "nodes": {
    }  }  }  }

    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 400
    response_json = response_content.json()
    print(json.dumps(response_json,sort_keys=True,indent=2))
    assert 'status' in response_json
    assert response_json['status'] == 'QueryGraphZeroNodes'


def test_edge_invalid_subject():
    query = {
      "submitter": 'ARAX_test',
      "message": {
        "query_graph": {
          "edges": {
            "e00": { "subject": "n99", "object": "n01", "predicates": ["biolink:physically_interacts_with"] }
          },
          "nodes": {
            "n00": { "categories": ["biolink:ChemicalSubstance"], "ids": ["CHEMBL.COMPOUND:CHEMBL112"] },
            "n01": { "categories": ["biolink:Protein"] }
    }  }  }  }

    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 400
    response_json = response_content.json()
    print(json.dumps(response_json,sort_keys=True,indent=2))
    assert 'status' in response_json
    assert response_json['status'] == 'QEdgeInvalidSubject'


def test_edge_invalid_object():
    query = {
      "submitter": 'ARAX_test',
      "message": {
        "query_graph": {
          "edges": {
            "e00": { "subject": "n00", "object": "n99", "predicates": ["biolink:physically_interacts_with"] }
          },
          "nodes": {
            "n00": { "categories": ["biolink:ChemicalSubstance"], "ids": ["CHEMBL.COMPOUND:CHEMBL112"] },
            "n01": { "categories": ["biolink:Protein"] }
    }  }  }  }

    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 400
    response_json = response_content.json()
    print(json.dumps(response_json,sort_keys=True,indent=2))
    assert 'status' in response_json
    assert response_json['status'] == 'QEdgeInvalidObject'


def test_no_curie():
    query = {
      "submitter": 'ARAX_test',
      "message": {
        "query_graph": {
          "edges": {
            "e00": { "subject": "n00", "object": "n01", "predicates": ["biolink:physically_interacts_with"] }
          },
          "nodes": {
            "n00": { "categories": ["biolink:ChemicalSubstance"] },
            "n01": { "categories": ["biolink:Protein"] }
    }  }  }  }

    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 400
    response_json = response_content.json()
    print(json.dumps(response_json,sort_keys=True,indent=2))
    assert 'status' in response_json
    assert response_json['status'] == 'QueryGraphNoIds'


def test_edge_extraneous_property():
    query = {
      "submitter": 'ARAX_test',
      "message": {
        "query_graph": {
          "edges": {
            "e00": { "subject": "n00", "object": "n01", "predicates": ["biolink:physically_interacts_with"], "reversable": True }
          },
          "nodes": {
            "n00": { "categories": ["biolink:ChemicalSubstance"], "ids": ["CHEMBL.COMPOUND:CHEMBL112"] },
            "n01": { "categories": ["biolink:Protein"] }
    }  }  }  }

    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 400
    response_json = response_content.json()
    print(json.dumps(response_json,sort_keys=True,indent=2))
    assert 'status' in response_json
    assert response_json['status'] == 'UnknownQEdgeProperty'


def test_node_extraneous_property():
    query = {
      "submitter": 'ARAX_test',
      "message": {
        "query_graph": {
          "edges": {
            "e00": { "subject": "n00", "object": "n01", "predicates": ["biolink:physically_interacts_with"] }
          },
          "nodes": {
            "n00": { "categories": ["biolink:ChemicalSubstance"], "ids": ["CHEMBL.COMPOUND:CHEMBL112"] },
            "n01": { "categories": ["biolink:Protein"], "set": True }             # Correct is "is_set" instead of "set"
    }  }  }  }

    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 400
    response_json = response_content.json()
    print(json.dumps(response_json,sort_keys=True,indent=2))
    assert 'status' in response_json
    assert response_json['status'] == 'UnknownQNodeProperty'


def test_workflow_bad_id():
    query = {
      "submitter": 'ARAX_test',
      "message": {
        "query_graph": {
          "edges": {
            "e00": { "subject": "n00", "object": "n01", "predicates": ["biolink:physically_interacts_with"] }
          },
          "nodes": {
            "n00": { "categories": ["biolink:ChemicalSubstance"], "ids": ["CHEMBL.COMPOUND:CHEMBL112"] },
            "n01": { "categories": ["biolink:Protein"], "is_set": True }
      }  }  },
      "workflow": [
        { "id": "filter_results_top_N", "parameters": { "max_results": 10 } }   # should be top_n
        ]
    }

    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 400
    response_json = response_content.json()
    print(json.dumps(response_json,sort_keys=True,indent=2))
    assert 'detail' in response_json
    assert 'not valid' in response_json['detail']


def test_workflow_bad_parameter():
    query = {
      "submitter": 'ARAX_test',
      "message": {
        "query_graph": {
          "edges": {
            "e00": { "subject": "n00", "object": "n01", "predicates": ["biolink:physically_interacts_with"] }
          },
          "nodes": {
            "n00": { "categories": ["biolink:ChemicalSubstance"], "ids": ["CHEMBL.COMPOUND:CHEMBL112"] },
            "n01": { "categories": ["biolink:Protein"], "is_set": True }
      }  }  },
      "workflow": [
        { "id": "filter_results_top_n", "parameters": { "max_results": "banana" } }   # should be an integer
        ]
    }

    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 400
    response_json = response_content.json()
    print(json.dumps(response_json,sort_keys=True,indent=2))
    assert 'detail' in response_json
    assert 'not valid' in response_json['detail']


def test_workflow_no_message():
    query = {
      "submitter": 'ARAX_test',
      "message": { },
      "workflow": [
        { "id": "filter_results_top_n", "parameters": { "max_results": 10 } }
        ]
    }

    response_content = requests.post(arax_url, json=query, headers={'accept': 'application/json'})
    status_code = response_content.status_code
    assert status_code == 400
    response_json = response_content.json()
    print(json.dumps(response_json,sort_keys=True,indent=2))
    assert 'status' in response_json
    assert response_json['status'] == 'MissingQueryGraph'


if __name__ == "__main__": pytest.main(['-v'])


# code/RTXConfiguration.py

#!/usr/bin/python3

import os
import datetime
import json
import time
import re
from typing import Optional

import yaml
from pygit2 import Repository, discover_repository


class RTXConfiguration:

    _GET_FILE_CMD = "scp araxconfig@araxconfig.rtx.ai:config_secrets.json "

    # ### Constructor
    def __init__(self):
        # Determine current ARAX and TRAPI versions
        file_dir = os.path.dirname(os.path.abspath(__file__))
        openapi_yaml_path = f"{file_dir}/UI/OpenAPI/python-flask-server/openapi_server/openapi/openapi.yaml"
        with open(openapi_yaml_path) as api_file:
            openapi_yaml = yaml.safe_load(api_file)
        self.arax_version = openapi_yaml["info"]["version"]
        self.trapi_version = openapi_yaml["info"]["x-trapi"]["version"]
        first_two_trapi_version_nums = self.trapi_version.split(".")[:2]
        self.trapi_major_version = ".".join(first_two_trapi_version_nums)
        self.version = f"ARAX {self.arax_version}"  # Not sure exactly what this is used for; legacy?

        # Grab instance/domain name info, if available
        self.instance_name = '??'
        match = re.match(r'/mnt/data/orangeboard/(.+)/RTX/code', file_dir)
        if match:
            self.instance_name = match.group(1)
        if self.instance_name == 'production':
            self.instance_name = 'ARAX'

        try:
            with open(f"{file_dir}/config.domain") as infile:
                for line in infile:
                    self.domain = line.strip()
        except:
            self.domain = '??'

        # Determine the branch we're running in
        repo_path = discover_repository(file_dir)
        try:
            repo = Repository(repo_path)
            self.current_branch_name = repo.head.name.split("/")[-1]
        except Exception:
            # TODO: Figure out why Docker container doesn't like this Git branch determination method
            # Ok to skip branch name here for now since domain name can be used instead in such cases
            self.current_branch_name = None

        # Determine our maturity
        maturity_override_value = self._read_override_file(f"{file_dir}/maturity_override.txt")
        if maturity_override_value:
            self.maturity = maturity_override_value
        else:
            # Otherwise we'll dynamically determine our maturity based on instance/domain name and/or branch
            if self.domain in ["arax.ci.transltr.io", "kg2.ci.transltr.io"]:
                self.maturity = "staging"
            elif self.domain in ["arax.test.transltr.io", "kg2.test.transltr.io"] or self.current_branch_name == "itrb-test":
                self.maturity = "testing"
            elif self.domain in ["arax.transltr.io", "kg2.transltr.io"] or self.current_branch_name == "production":
                self.maturity = "production"
            elif self.domain == "arax.ncats.io":
                if self.instance_name in ["ARAX", "kg2"] or self.current_branch_name == "production":
                    self.maturity = "production"
                else:
                    self.maturity = "development"
            else:
                self.maturity = "development"

        # Determine if this is an ITRB instance or our CICD instance
        self.is_itrb_instance = "transltr.io" in self.domain  # Hacky, but works

        config_secrets_file_path = os.path.dirname(os.path.abspath(__file__)) + '/config_secrets.json'
        config_dbs_file_path = os.path.dirname(os.path.abspath(__file__)) + '/config_dbs.json'
        config_secrets_local_file_path = os.path.dirname(os.path.abspath(__file__)) + '/config_secrets_local.json'

        # Download the latest copy of config_secrets.json as appropriate (or override by local file, if present)
        if os.path.exists(config_secrets_local_file_path):
            config_secrets_file_path = config_secrets_local_file_path
        elif not os.path.exists(config_secrets_file_path):
            # scp the file
            os.system(RTXConfiguration._GET_FILE_CMD + config_secrets_file_path)
        else:
            now_time = datetime.datetime.now()
            modified_time = time.localtime(os.stat(config_secrets_file_path).st_mtime)
            modified_time = datetime.datetime(*modified_time[:6])
            if (now_time - modified_time).days > 0:
                # scp the file
                os.system(RTXConfiguration._GET_FILE_CMD + config_secrets_file_path)

        # Load the contents of the two config files (config_dbs.json lives in the repo; no need to download)
        with open(config_secrets_file_path, 'r') as config_secrets_file:
            self.config_secrets = json.load(config_secrets_file)
        with open(config_dbs_file_path, 'r') as config_dbs_file:
            self.config_dbs = json.load(config_dbs_file)

        # AG: Not sure exactly what this is doing?
        self.is_production_server = False
        if ( ( 'HOSTNAME' in os.environ and os.environ['HOSTNAME'] == '6d6766e08a31' ) or
            ( 'PWD' in os.environ and 'mnt/data/orangeboard' in os.environ['PWD'] ) ):
            self.is_production_server = True

        # Set database file paths
        self.db_host = "arax.ncats.io"
        self.db_username = "rtxconfig"
        database_downloads = self.config_dbs["database_downloads"]
        self.cohd_database_path = database_downloads["cohd_database"]
        self.cohd_database_version = self.cohd_database_path.split('/')[-1].split('_v')[-1].replace('.db', '')
        self.graph_database_path = database_downloads["graph_database"]
        self.graph_database_version = self.graph_database_path.split('/')[-1].split('_v')[-1].replace('.sqlite', '')
        self.log_model_path = database_downloads["log_model"]
        self.log_model_version = self.log_model_path.split('/')[-1].split('_v')[-1].replace('.pkl', '')
        self.curie_to_pmids_path = database_downloads["curie_to_pmids"]
        self.curie_to_pmids_version = self.curie_to_pmids_path.split('/')[-1].split('_v')[-1].replace('.sqlite', '')
        self.node_synonymizer_path = database_downloads["node_synonymizer"]
        self.node_synonymizer_version = self.node_synonymizer_path.split('/')[-1].split('_v')[-1].replace('.sqlite', '')
        self.dtd_prob_path = database_downloads["dtd_prob"]
        self.dtd_prob_version = self.dtd_prob_path.split('/')[-1].split('_v')[-1].replace('.db', '')
        self.kg2c_sqlite_path = database_downloads["kg2c_sqlite"]
        self.kg2c_sqlite_version = self.kg2c_sqlite_path.split('/')[-1].split('_v')[-1].replace('.sqlite', '')
        self.kg2c_meta_kg_path = database_downloads["kg2c_meta_kg"]
        self.kg2c_meta_kg_version = self.kg2c_meta_kg_path.split('/')[-1].split('_v')[-1].replace('.json', '')
        self.fda_approved_drugs_path = database_downloads["fda_approved_drugs"]
        self.fda_approved_drugs_version = self.fda_approved_drugs_path.split('/')[-1].split('_v')[-1].replace('.pickle', '')
        self.autocomplete_path = database_downloads["autocomplete"]
        self.autocomplete_version = self.autocomplete_path.split('/')[-1].split('_v')[-1].replace('.sqlite', '')
        self.explainable_dtd_db_path = database_downloads["explainable_dtd_db"]
        self.explainable_dtd_db_version = self.explainable_dtd_db_path.split('/')[-1].split('_v')[-1].replace('.db', '')

        # Set up mysql feedback
        self.mysql_feedback_host = self.config_secrets["mysql_feedback"]["host"]
        self.mysql_feedback_port = self.config_secrets["mysql_feedback"]["port"]
        self.mysql_feedback_username = self.config_secrets["mysql_feedback"]["username"]
        self.mysql_feedback_password = self.config_secrets["mysql_feedback"]["password"]

        # Set up correct Plover URL (since it's not registered in SmartAPI)
        plover_url_override_value = self._read_override_file(f"{file_dir}/plover_url_override.txt")
        if plover_url_override_value:
            self.plover_url = plover_url_override_value
        elif self.maturity in {"production", "prod"}:
            self.plover_url = self.config_dbs["plover"]["prod"]
        elif self.maturity in {"testing", "test"}:
            self.plover_url = self.config_dbs["plover"]["test"]
        else:  # Includes staging, development
            self.plover_url = self.config_dbs["plover"]["dev"]

        # Set KG2 url if an override was provided
        kg2_url_override_value = self._read_override_file(f"{file_dir}/kg2_url_override.txt")
        if kg2_url_override_value:
            self.rtx_kg2_url = kg2_url_override_value
        else:
            self.rtx_kg2_url = None

        # Default to KG2c neo4j
        self.neo4j_kg2 = "KG2c"

    @staticmethod
    def _read_override_file(file_path: str) -> Optional[str]:
        if os.path.exists(file_path):
            with open(file_path, 'r') as override_file:
                lines = override_file.readlines()
                if not lines or not lines[0]:
                    raise ValueError(f"{file_path} exists but does not contain anything! "
                                     f"It should be a single-line file containing the override value.")
                else:
                    return lines[0].strip()
        else:
            return None

    # ### Define attribute version
    @property
    def version(self) -> str:
        return self._version

    @version.setter
    def version(self, version: str):
        self._version = version

    @property
    def neo4j_kg2(self) -> str:
        return self._neo4j_kg2

    @neo4j_kg2.setter
    def neo4j_kg2(self, neo4j_kg2: str):
        self._neo4j_kg2 = neo4j_kg2

        if self.neo4j_kg2 not in self.config_dbs["neo4j"].keys():
            self.neo4j_bolt = None
            self.neo4j_database = None
            self.neo4j_username = None
            self.neo4j_password = None
        else:
            neo4j_instance = self.config_dbs["neo4j"][self.neo4j_kg2]
            self.neo4j_bolt = f"bolt://{neo4j_instance}:7687"
            self.neo4j_database = f"{neo4j_instance}:7474/db/data"
            self.neo4j_username = self.config_secrets["neo4j"][self.neo4j_kg2]["username"]
            self.neo4j_password = self.config_secrets["neo4j"][self.neo4j_kg2]["password"]

    @property
    def neo4j_bolt(self) -> str:
        return self._neo4j_bolt

    @neo4j_bolt.setter
    def neo4j_bolt(self, bolt: str):
        self._neo4j_bolt = bolt

    @property
    def neo4j_database(self) -> str:
        return self._neo4j_database

    @neo4j_database.setter
    def neo4j_database(self, database: str):
        self._neo4j_database = database

    @property
    def neo4j_username(self) -> str:
        return self._neo4j_username

    @neo4j_username.setter
    def neo4j_username(self, username: str):
        self._neo4j_username = username

    @property
    def neo4j_password(self) -> str:
        return self._neo4j_password

    @neo4j_password.setter
    def neo4j_password(self, password: str):
        self._neo4j_password = password

    @property
    def plover_url(self) -> str:
        return self._plover_url

    @plover_url.setter
    def plover_url(self, url: str):
        self._plover_url = url

    @property
    def mysql_feedback_host(self) -> str:
        return self._mysql_feedback_host

    @mysql_feedback_host.setter
    def mysql_feedback_host(self, host: str):
        self._mysql_feedback_host = host

    @property
    def mysql_feedback_port(self) -> str:
        return self._mysql_feedback_port

    @mysql_feedback_port.setter
    def mysql_feedback_port(self, port: str):
        self._mysql_feedback_port = port

    @property
    def mysql_feedback_username(self) -> str:
        return self._mysql_feedback_username

    @mysql_feedback_username.setter
    def mysql_feedback_username(self, username: str):
        self._mysql_feedback_username = username

    @property
    def mysql_feedback_password(self) -> str:
        return self._mysql_feedback_password

    @mysql_feedback_password.setter
    def mysql_feedback_password(self, password: str):
        self._mysql_feedback_password = password


def main():
    rtxConfig = RTXConfiguration()
    print("RTX Version string: " + rtxConfig.version)
    print("neo4j KG2 version: %s" % rtxConfig.neo4j_kg2)
    print("neo4j bolt: %s" % rtxConfig.neo4j_bolt)
    print("neo4j database: %s" % rtxConfig.neo4j_database)
    print("neo4j username: %s" % rtxConfig.neo4j_username)
    print("neo4j password: %s" % rtxConfig.neo4j_password)
    print("plover url: %s" % rtxConfig.plover_url)
    print("rtx-kg2 url: %s" % rtxConfig.rtx_kg2_url)
    print("mysql feedback host: %s" % rtxConfig.mysql_feedback_host)
    print("mysql feedback port: %s" % rtxConfig.mysql_feedback_port)
    print("mysql feedback username: %s" % rtxConfig.mysql_feedback_username)
    print("mysql feedback password: %s" % rtxConfig.mysql_feedback_password)
    print(f"maturity: {rtxConfig.maturity}")
    print(f"current branch: {rtxConfig.current_branch_name}")
    print(f"is_itrb_instance: {rtxConfig.is_itrb_instance}")


if __name__ == "__main__":
    main()


# code/ARAX/NodeSynonymizer/category_manager.py

#!/usr/bin/python3
""" A class to access BioLink categories
"""

import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast
import time
import pickle
import re
import copy

import requests
import requests_cache

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../BiolinkHelper/")
from biolink_helper import BiolinkHelper


# Class that provides a simple interface to BioLink categories and their ancestors and approved conflations
class CategoryManager:


    # Constructor
    def __init__(self):
        self.location = os.path.dirname(os.path.abspath(__file__))
        requests_cache.install_cache(self.location + '/category_manager.cache')
        self.bh = BiolinkHelper()

        self.categories = {
            'ancestors': {},
            'relevant_categories': {}
        }

        self.approved_conflations = {
            'biolink:Gene': [ 'biolink:Protein' ],
            'biolink:Protein': [ 'biolink:Gene' ],
            # Decided 2021-07-28 mini-hackathon that we may be best off NOT doing conflation here. Just use
            # ChemicalEntity to refer to everything
            # #'biolink:Drug': [ 'biolink:ChemicalEntity', 'biolink:MolecularEntity', 'biolink:SmallMolecule' ],
            #'biolink:ChemicalEntity': [ 'biolink:Drug', 'biolink:MolecularEntity', 'biolink:SmallMolecule' ],
            #'biolink:SmallMolecule': [ 'biolink:Drug', 'biolink:MolecularEntity', 'biolink:ChemicalEntity' ],
            #'biolink:MolecularEntity': [ 'biolink:Drug', 'biolink:SmallMolecule', 'biolink:ChemicalEntity' ],
            'biolink:Disease': [ 'biolink:PhenotypicFeature' ],
            'biolink:PhenotypicFeature': [ 'biolink:Disease' ],
            'biolink:DiseaseOrPhenotypicFeature': [ 'biolink:Disease', 'biolink:PhenotypicFeature' ],
        }



    # ############################################################################################
    # Store the cache of all category information
    # Not actually used. Rely on web cache for now
    def store_cache(self):
        if self.categories is None:
            return
        filename = self.location + '/category_manager.pickle'
        print(f"INFO: Storing category_manager cache to {filename}")
        with open(filename, "wb") as outfile:
            pickle.dump(self.categories, outfile)


    # ############################################################################################
    # Load the cache of all category information
    # Not actually used. Rely on web cache for now
    def load_cache(self):
        filename = self.location + '/category_manager.pickle'
        if os.path.exists(filename):
            print(f"INFO: Reading category_manager cache from {filename}")
            with open(filename, "rb") as infile:
                self.categories = pickle.load(infile)
        else:
            print(f"INFO: category_manager cache {filename} does not yet exist. Need to create it first.")


    # ############################################################################################
    # Retrieve the ancestors of a biolink category from SRI web service
    def get_ancestors(self, category):

        if category is None:
            return

        if not isinstance(category,str):
            raise(f"ERROR: category {category} is not type str")

        # If we alreacy have the information, return the cached result
        if category in self.categories['ancestors']:
            return self.categories['ancestors'][category]

        # Build the URL and fetch the result
        url = f"https://bl-lookup-sri.renci.org/bl/{category}/ancestors?version={self.bh.get_current_arax_biolink_version()}"
        response_content = requests.get(url, headers={'accept': 'application/json'})
        status_code = response_content.status_code

        # Check for a returned error
        if status_code != 200:
            eprint(f"WARNING: returned with status {status_code} while retrieving ancestors for {category}")
            response_list = [ category ]
            self.categories['ancestors'][category] = response_list
            return response_list

        # Unpack the response
        response_list = response_content.json()

        self.categories['ancestors'][category] = response_list

        return response_list


    # ############################################################################################
    # Retrieve the ancestors of a biolink category
    def get_expansive_categories(self, categories):

        # If no categories are provided, then there's nothing to do
        if categories is None:
            return

        # If the supplied categories is just a string, then turn is into the expected list
        if isinstance(categories,str):
            categories = [ categories ]

        # If the supplied categories is not a list, then error out
        if not isinstance(categories,list):
            raise(f"ERROR: categories {categories} must be type list or str")

        # Create a dict to store of the computed expansive categories
        expansive_categories = {}

        # Loop over all the supplied categories and merge them into a single list
        for category in categories:

            # First add the category itself plus any approved conflations
            expansive_categories[category] = True
            if category in self.approved_conflations:
                for conflated_category in self.approved_conflations[category]:
                    expansive_categories[conflated_category] = True

            parent_expansive_categories = copy.copy(expansive_categories)
            for parent_category in parent_expansive_categories:

                # Get the ancestors of this category
                ancestors = self.get_ancestors(parent_category)

                # If we didn't get any, that's fine, just move on
                if ancestors is None:
                    continue

                # For each discovered ancestor, add it to the dict and also add any approved conflations
                for ancestor in ancestors:
                    if ancestor == 'biolink:Entity':
                        continue
                    expansive_categories[ancestor] = True
                    if ancestor in self.approved_conflations:
                        for conflated_category in self.approved_conflations[ancestor]:
                            expansive_categories[conflated_category] = True


        return expansive_categories




# ############################################################################################
# Command line interface for this class
def main():

    import argparse
    parser = argparse.ArgumentParser(
        description="Interface to the Category Manager", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('-b', '--build', action="store_true",
                        help="If set, (re)build the local SRI Node Normalizer cache from scratch", default=False)
    parser.add_argument('-a', '--ancestors', action="store",
                        help="Look up the ancestors for the supplied category", default=False)
    parser.add_argument('-c', '--categories', action="store",
                        help="Get the full list of exansive categories for the supplied category", default=False)
    args = parser.parse_args()

    if not args.build and not args.ancestors and not args.categories:
        parser.print_help()
        sys.exit(2)

    catman = CategoryManager()

    if args.ancestors:
        ancestors = catman.get_ancestors(args.ancestors)
        print(json.dumps(ancestors, indent=2, sort_keys=True))
        return

    if args.categories:
        categories = catman.get_expansive_categories(args.categories)
        print(json.dumps(categories, indent=2, sort_keys=True))
        return

if __name__ == "__main__": main()






# code/ARAX/NodeSynonymizer/dump_kg2_node_data.py

"""
This script dumps KG2 node data to three files: one containing node ID, name, full_name, type for all KG2 nodes,
another containing node synonyms, and another containing pairs of equivalent nodes (connected by same_as edges
in KG2).
These output files are used in the NodeSynonymizer build process.
Usage: python3 dump_kg2_node_data.py [--test]
"""
import argparse
import csv
import json
import traceback
from typing import List, Dict
import os
import sys
import re
from neo4j import GraphDatabase

import numpy as np
np.warnings.filterwarnings('ignore')

sys.path.append(os.path.dirname(os.path.abspath(__file__))+"/../../")  # code directory
from RTXConfiguration import RTXConfiguration
remove_tab_newlines = re.compile(r"\s+")


def dump_kg2_node_info(file_name: str, write_mode: str, is_test: bool):
	"""
	Dump node id, name, full_name, and category of all nodes in KG2.
	:param file_name: name of file to save to (TSV)
	:param write_mode: 'w' for overwriting the file, 'a' for appending to the file at the end (or creating a new on if it DNE)
	:param is_test: True if this is a test, False otherwise
	:return: None
	"""
	query = f"match (n) return properties(n) as p, labels(n) as l {'limit 20' if is_test else ''}"
	res = _run_cypher_query(query)
	if res:
		with open(file_name, write_mode, encoding="utf-8") as fid:
			for item in res:
				prop_dict = item['p']
				labels = item['l']
				try:
					label = list(set(labels) - {'Base'}).pop()
				except:
					label = ""
				try:
					fid.write('%s\t' % prop_dict['id'])
				except:
					fid.write('\t')
				try:
					fid.write('%s\t' % remove_tab_newlines.sub(" ", prop_dict['name']))  # better approach
				except:
					fid.write('\t')
				try:
					fid.write('%s\t' % remove_tab_newlines.sub(" ", prop_dict['full_name']))
				except:
					fid.write('\t')
				try:
					fid.write('%s\n' % label)
				except:
					fid.write('\n')
		print(f"Successfully created file '{file_name}'.")
	else:
		raise Exception(f"Failed to get results from Neo4j for {file_name}")
	return


def _run_cypher_query(cypher_query: str) -> List[Dict[str, any]]:
	# This function sends a cypher query to the KG2 neo4j and returns results
	rtxc = RTXConfiguration()
	rtxc.neo4j_kg2 = "KG2pre"
	try:
		driver = GraphDatabase.driver(rtxc.neo4j_bolt, auth=(rtxc.neo4j_username, rtxc.neo4j_password))
		with driver.session() as session:
			print(f"Sending cypher query to KG2 neo4j ({rtxc.neo4j_bolt})")
			query_results = session.run(cypher_query).data()
			print(f"Got {len(query_results)} results back from neo4j")
		driver.close()
	except Exception:
		tb = traceback.format_exc()
		error_type, error, _ = sys.exc_info()
		print(f"Encountered an error interacting with KG2 neo4j. {tb}")
		return []
	else:
		return query_results


def dump_kg2_equivalencies(output_file_name: str, is_test: bool):
	# This function creates a TSV file of node pairs linked by a 'same_as' relationship in KG2
	cypher_query = f"match (n1)-[:`biolink:same_as`]->(n2) return distinct n1.id, n2.id {'limit 20' if is_test else ''}"
	equivalent_node_pairs = _run_cypher_query(cypher_query)
	if equivalent_node_pairs:
		with open(output_file_name, "w+") as output_file:
			csv_writer = csv.writer(output_file, delimiter='\t')
			csv_writer.writerow(list(equivalent_node_pairs[0].keys()))  # Add header row
			distinct_pairs = {tuple(sorted([node_pair["n1.id"], node_pair["n2.id"]])) for node_pair in equivalent_node_pairs}
			csv_writer.writerows(list(distinct_pairs))
		print(f"Successfully created file '{output_file_name}'.")
	else:
		raise Exception(f"Failed to get results from Neo4j for {output_file_name}")


def dump_kg2_synonym_field(output_file_name: str, is_test: bool):
	# This function creates a JSON file of KG2 nodes and their synonyms as listed in the node "synonym" property
	cypher_query = f"match (n) where n.synonym is not null return n.id, n.synonym {'limit 20' if is_test else ''}"
	results = _run_cypher_query(cypher_query)
	if results:
		synonym_map = dict()
		with open(output_file_name, 'w+') as output_file:
			for row in results:
				curie = row['n.id']
				synonym_map[curie] = row['n.synonym']
			json.dump(synonym_map, output_file)
		print(f"Successfully created file '{output_file_name}'.")
	else:
		raise Exception(f"Failed to get results from Neo4j for {output_file_name}")


def main():
	arg_parser = argparse.ArgumentParser()
	arg_parser.add_argument('--test', dest='test', action='store_true', default=False)
	args = arg_parser.parse_args()
	is_test = args.test

	dump_kg2_node_info('kg2_node_info.tsv', 'w', is_test)
	dump_kg2_equivalencies('kg2_equivalencies.tsv', is_test)
	dump_kg2_synonym_field('kg2_synonyms.json', is_test)
	print("Done dumping KG2 node data!")


if __name__ == "__main__":
	main()


# code/ARAX/NodeSynonymizer/sri_node_normalizer.py

#!/usr/bin/python3
""" An interface to the SRI Node and Edge Normalizer https://nodenormalization-sri.renci.org/docs
    e.g.:  https://nodenormalization-sri.renci.org/1.3/get_normalized_nodes?curie=CHEMBL.COMPOUND:CHEMBL76729
    Report problems here: https://github.com/TranslatorSRI/NodeNormalization/issues
"""

import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast
import time
import pickle
import re
import platform
import sqlite3
import requests
import requests_cache


BASE_URL = 'https://nodenormalization-sri.renci.org/1.3'

# Class that provides a simple interface to the SRI Node normalizer
class SriNodeNormalizer:


    # Constructor
    def __init__(self):
        requests_cache.install_cache('sri_node_normalizer_requests_cache')

        self.supported_types = None
        self.supported_prefixes = None
        self.cache = {}
        self.stats = {}

        # Translation table of different curie prefixes ARAX -> normalizer
        self.curie_prefix_tx_arax2sri = {
            #'REACT': 'Reactome',
            #'Orphanet': 'ORPHANET',
            #'ICD10': 'ICD-10',
        }
        self.curie_prefix_tx_sri2arax = {
            #'Reactome': 'REACT',
            #'ORPHANET': 'Orphanet',
            #'ICD-10': 'ICD10',
        }


    # ############################################################################################
    # Store the cache of all normalizer results
    def store_cache(self):
        if self.cache is None:
            return
        filename = f"sri_node_normalizer_curie_cache.pickle"
        print(f"INFO: Storing SRI normalizer cache to {filename}")
        with open(filename, "wb") as outfile:
            pickle.dump(self.cache, outfile)


    # ############################################################################################
    # Load the cache of all normalizer results
    def load_cache(self):
        filename = f"sri_node_normalizer_curie_cache.pickle"
        if os.path.exists(filename):
            print(f"INFO: Reading SRI normalizer cache from {filename}")
            with open(filename, "rb") as infile:
                self.cache = pickle.load(infile)
        else:
            print(f"INFO: SRI node normalizer cache {filename} does not yet exist. Need to fill it.")


    # ############################################################################################
    # Fill the cache with KG nodes
    def fill_cache(self):

        # Get a hash of curie prefixes supported
        self.get_supported_prefixes()

        filename = 'kg2_node_info.tsv'
        filesize = os.path.getsize(filename)

        # Correction for Windows line endings
        extra_bytes = 0
        fh = open(filename, 'rb')
        if platform.system() == 'Windows' and b'\r\n' in fh.read():
            print('WARNING: Windows line ending requires bytes_read compenstation')
            extra_bytes = 1
        fh.close()

        # Open the file and read in the curies
        fh = open(filename, 'r', encoding="latin-1", errors="replace")
        print(f"INFO: Reading {filename} to pre-fill the normalizer cache")
        previous_percentage = -1
        line_counter = 0
        supported_curies = 0
        bytes_read = 0

        # Create dicts to hold all the information
        batch = []

        # Loop over each line in the file
        for line in fh:
            bytes_read += len(line) + extra_bytes

            match = re.match(r'^\s*$',line)
            if match:
                continue
            columns = line.strip().split("\t")
            node_curie = columns[0]

            curie_prefix = node_curie.split(':')[0]

            # If we use different curie prefixes than the normalizer, need to fix
            normalizer_curie_prefix = curie_prefix
            normalizer_node_curie = node_curie
            if curie_prefix in self.curie_prefix_tx_arax2sri:
                normalizer_curie_prefix = self.curie_prefix_tx_arax2sri[curie_prefix]
                normalizer_node_curie = re.sub(curie_prefix,normalizer_curie_prefix,node_curie)

            # Decide if we want to keep this curie in the batch of things to look up
            # By default, no
            keep = 0
            # If this is a curie prefix that is supported by the normalizer, then yes
            if normalizer_curie_prefix in self.supported_prefixes:
                keep = 1
            # Unless it's already in the cache, then no
            if normalizer_node_curie in self.cache:
                keep = 0
            # Or if we've reached the end of the file, then set keep to 99 and trigger end-of-file processing of the last batch
            if bytes_read + 3 > filesize and len(batch) > 0:
                keep = 99

            # If we want to put this curie in the batch, or drain the batch at end-of-file
            if keep:

                if keep == 1:
                    supported_curies += 1
                    batch.append(normalizer_node_curie)

                if len(batch) > 500 or keep == 99:
                    if bytes_read + 3 > filesize:
                        print("Drain final batch")
                    results = self.get_node_normalizer_results(batch)
                    print(".", end='', flush=True)
                    for curie in batch:
                        if curie in self.cache:
                            continue
                        curie_prefix = curie.split(':')[0]
                        if curie_prefix not in self.stats:
                            self.stats[curie_prefix] = { 'found': 0, 'not found': 0, 'total': 0 }
                        if results is None or curie not in results or results[curie] is  None:
                            self.cache[curie] = None
                            self.stats[curie_prefix]['not found'] += 1
                        else:
                            self.cache[curie] = results[curie]
                            self.stats[curie_prefix]['found'] += 1
                        self.stats[curie_prefix]['total'] += 1

                    # Clear the batch list
                    batch = []

            # Print out some progress information
            line_counter += 1
            percentage = int(bytes_read*100.0/filesize)
            if percentage > previous_percentage:
                previous_percentage = percentage
                print(str(percentage)+"%..", end='', flush=True)

        # Close and summarize
        fh.close()
        print("")
        print(f"{line_counter} lines read")
        print(f"{bytes_read} bytes read of {filesize} bytes in file")
        print(f"{supported_curies} curies with prefixes supported by the SRI normalizer")

        print("Build stats:")
        print(json.dumps(self.stats, indent=2, sort_keys=True))

        # Store or sync the results
        self.store_cache()

    def load_cache_from_sqlite(self):
        dbfile = "sri_node_normalizer_requests_cache.sqlite"
        conn = sqlite3.connect(dbfile)
        cur = conn.cursor()
        response = cur.execute('''SELECT * from responses''')
        print("Responses")
        print(response.fetchall())
        

    # ############################################################################################
    # Retrieve the dict of supported BioLink types
    def get_supported_types(self):

        # If we've already done this before, return the cached result
        if self.supported_types is not None:
            return self.supported_types

        # Build the URL and fetch the result
        url = f"{BASE_URL}/get_semantic_types"
        response_content = self.load_cache_from_sqlite()#requests.get(url, headers={'accept': 'application/json'})
        #status_code = response_content.status_code

        # Check for a returned error
        # if status_code != 200:
        #     eprint(f"ERROR returned with status {status_code} while retrieving supported types")
        #     eprint(response_content)
        #     exit(211)

        # Unpack the response into a dict and return it
        response_dict = response_content.json()
        if 'semantic_types' not in response_dict:
            eprint(f"ERROR Did not find expected 'semantic_types'")
            return
        if 'types' not in response_dict['semantic_types']:
            eprint(f"ERROR Did not find expected 'types' list")
            return

        node_types = {}
        for node_type in response_dict['semantic_types']['types']:
            node_types[node_type] = 1

        if len(node_types) == 0:
            node_types = None

        # Save this for future use
        self.supported_types = node_types

        return node_types


    # ############################################################################################
    # Retrieve the dict of supported curie prefixes
    def get_supported_prefixes(self):

        # If we've already done this before, return the cached result
        if self.supported_prefixes is not None:
            return self.supported_prefixes
        supported_prefixes = {}

        # Build the URL and fetch the result
        url = f"{BASE_URL}/get_curie_prefixes"
        response_content = requests.get(url, headers={'accept': 'application/json'})
        status_code = response_content.status_code

        # Check for a returned error
        if status_code != 200:
            eprint(f"ERROR returned with status {status_code} while retrieving supported types")
            eprint(response_content)
            exit(253)

        # Unpack the response into a dict and return it
        response_dict = response_content.json()
        for entity_name,entity in response_dict.items():
            if 'curie_prefix' not in entity:
                eprint(f"ERROR Did not find expected 'curie_prefix'")
                return
            for curie_prefix,count in entity['curie_prefix'].items():
                if str(count) != 'Not found':
                    supported_prefixes[curie_prefix] = count

        # Save this for future use
        self.supported_prefixes = supported_prefixes
        return supported_prefixes


    # ############################################################################################
    # Directly fetch a normalization for a CURIE from the Normalizer
    def get_node_normalizer_results(self, curies, cache_only=None):

        if isinstance(curies,str):
            #print(f"INFO: Looking for curie {curies}")
            if curies in self.cache:
                #print(f"INFO: Using prefill cache for lookup on {curies}")
                result = { curies: self.cache[curies] }
                return result
            curies = [ curies ]

        if cache_only is not None:
            print(f"ERROR: Call to sri_node_normalizer requested cache_only and we missed the cache with {curies}")

        # Build the URL and fetch the result
        url = f"{BASE_URL}/get_normalized_nodes"

        #eprint(f"{len(url)},")

        retry = 0
        sleep_time = 5
        error_state = True

        while retry < 3 and error_state:

            error_state = False

            try:
                response_content = requests.post(url, json={"curies": curies}, headers={'accept': 'application/json'})
            except:
                print("Uncaught error during web request to SRI normalizer")
                error_state = True
                status_code = 0
                response_content = "uncaught error - no response content"
            else:
                status_code = response_content.status_code

            # Check for a returned error
            if status_code == 404:
                #eprint(f"INFO: No normalization data for {curie}")
                return
            elif status_code != 200:
                eprint(f"ERROR returned with status {status_code} while searching with curie list of length {len(curies)} including {curies[0]}")
                eprint(response_content)
                error_state = True

            if error_state:
                print(f"Try again after {sleep_time} seconds")
                time.sleep(sleep_time)
                retry += 1
                sleep_time += 20

        if error_state:
            return

        # Unpack the response into a dict and return it
        response_dict = response_content.json()
        return response_dict


    # ############################################################################################
    # Return a simple dict with the equivalence information and metadata about a CURIE
    def get_empty_equivalence(self, curie=''):

        response = { 'status': 'EMPTY', 'curie': curie, 'preferred_curie': '', 'preferred_curie_name': '',
            'type': '', 'equivalent_identifiers': [], 'equivalent_names': [] }
        return response


    # ############################################################################################
    # Return a simple dict with the equivalence information and metadata about a CURIE
    def get_curie_equivalence(self, curie, cache_only=None):

        response = { 'status': 'ERROR', 'curie': curie, 'preferred_curie': '', 'preferred_curie_name': '',
            'type': '', 'equivalent_identifiers': [], 'equivalent_names': [] }

        # Do a translation for different curie prefixes
        curie_prefix = curie.split(':')[0]
        normalizer_curie = curie
        if curie_prefix in self.curie_prefix_tx_arax2sri:
            normalizer_curie = re.sub(curie_prefix,self.curie_prefix_tx_arax2sri[curie_prefix],curie)

        results = self.get_node_normalizer_results(normalizer_curie, cache_only=cache_only)
        #print(json.dumps(results, indent=2, sort_keys=True))

        if results is None:
            response['status'] = 'no information'
            return response

        # If input CURIE is not the key of the dict, this is highly unexpected
        if normalizer_curie not in results:
            eprint(f"ERROR: Did not find the curie {normalizer_curie} as a key in the results")
            return response

        if results[normalizer_curie] is None:
            response['status'] = 'no information'
            return response

        # If there is no id in the results, this a highly unexpected
        if 'id' not in results[normalizer_curie]:
            eprint(f"ERROR: Did not find 'id' as a key in the results from the SRI normalizer for curie {normalizer_curie}")
            return response

        # If there is a preferred CURIE, store it and its name
        response['preferred_curie'] = results[normalizer_curie]['id']['identifier']
        if 'label' in results[normalizer_curie]['id']:
            response['preferred_curie_name'] = results[normalizer_curie]['id']['label']

        # Translate the id if necessary
        if curie != normalizer_curie:
            response['preferred_curie'] = re.sub(self.curie_prefix_tx_arax2sri[curie_prefix],curie_prefix,results[normalizer_curie]['id']['identifier'])

        # If there is a returned type, store it
        if 'type' in results[normalizer_curie]:
            node_type = results[normalizer_curie]['type'][0]
            response['type'] = node_type

        # If there are additional equivalent identifiers and names, store them
        names = {}
        if 'equivalent_identifiers' in results[normalizer_curie]:
            for equivalence in results[normalizer_curie]['equivalent_identifiers']:
                if 'identifier' in equivalence:
                    id = equivalence['identifier']
                    if curie != normalizer_curie:
                        id = re.sub(self.curie_prefix_tx_arax2sri[curie_prefix],curie_prefix,id)
                    #response['equivalent_identifiers'].append(id)
                    response['equivalent_identifiers'].append(equivalence)
                if 'label' in equivalence:
                    if equivalence['label'] not in names:
                        response['equivalent_names'].append(equivalence['label'])

        response['status'] = 'OK'
        return response


# ############################################################################################
# Command line interface for this class
def main():

    import argparse
    parser = argparse.ArgumentParser(
        description="Interface to the SRI Node Normalizer", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('-b', '--build', action="store_true",
                        help="If set, (re)build the local SRI Node Normalizer cache from scratch", default=False)
    parser.add_argument('-l', '--load', action="store_true",
                        help="If set, load the previously built local SRI Node Normalizer cache", default=False)
    parser.add_argument('-c', '--curie', action="store",
                        help="Specify a curie to look up with the SRI Node Normalizer (e.g., UniProtKB:P01308, ORPHANET:2322, DRUGBANK:DB11655", default=None)
    parser.add_argument('-p', '--prefixes', action="store_true",
                        help="If set, list the SRI Node Normalizer supported prefixes", default=None)
    parser.add_argument('-t', '--types', action="store_true",
                        help="If set, list the SRI Node Normalizer supported types", default=None)
    args = parser.parse_args()

    if not args.build and not args.curie and not args.prefixes and not args.types:
        parser.print_help()
        sys.exit(2)

    normalizer = SriNodeNormalizer()

    if args.prefixes:
        supported_prefixes = normalizer.get_supported_prefixes()
        print(json.dumps(supported_prefixes, indent=2, sort_keys=True))
        return

    if args.types:
        supported_types = normalizer.get_supported_types()
        print(json.dumps(supported_types, indent=2, sort_keys=True))
        return

    if args.build:
        print("INFO: Beginning SRI Node Normalizer cache building process for KG2. Make sure you have a good network connection as this will download ~2 GB of data.")
        print("INFO: Note that requests-cache is used, so the sri_node_normalizer_requests_cache.sqlite file should be deleted first if it might be stale.")
        normalizer.fill_cache()
        normalizer.store_cache()
        print("INFO: Build process complete")
        return

    if args.load:
        normalizer.load_cache()

    curie = 'UniProtKB:P01308'
    if args.curie:
        curie = args.curie

    #print(platform.system())

    print("==========================================================")
    print("Native SRI Node Normalizer results:")
    normalized_results = normalizer.get_node_normalizer_results(curie)
    print(json.dumps(normalized_results, indent=2, sort_keys=True))

    print("==========================================================")
    print("Local more compact and useful formatting:")
    equivalence = normalizer.get_curie_equivalence(curie)
    print(json.dumps(equivalence, indent=2, sort_keys=True))


if __name__ == "__main__": main()






# code/ARAX/NodeSynonymizer/sri_node_normalizer_workaround.py
#!/usr/bin/python3
""" An interface to the SRI Node and Edge Normalizer https://nodenormalization-sri.renci.org/docs
    e.g.:  https://nodenormalization-sri.renci.org/1.3/get_normalized_nodes?curie=CHEMBL.COMPOUND:CHEMBL76729
    Report problems here: https://github.com/TranslatorSRI/NodeNormalization/issues
"""

import sys
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

import os
import json
import ast
import time
import pickle
import re
import platform

import requests
import requests_cache


BASE_URL = 'https://nodenormalization-sri.renci.org/1.3'

# Class that provides a simple interface to the SRI Node normalizer
class SriNodeNormalizer:


    # Constructor
    def __init__(self):
        requests_cache.install_cache('sri_node_normalizer_requests_cache')

        self.supported_types = None
        self.supported_prefixes = None
        self.cache = {}
        self.stats = {}

        # Translation table of different curie prefixes ARAX -> normalizer
        self.curie_prefix_tx_arax2sri = {
            #'REACT': 'Reactome',
            #'Orphanet': 'ORPHANET',
            #'ICD10': 'ICD-10',
        }
        self.curie_prefix_tx_sri2arax = {
            #'Reactome': 'REACT',
            #'ORPHANET': 'Orphanet',
            #'ICD-10': 'ICD10',
        }


    # ############################################################################################
    # Store the cache of all normalizer results
    def store_cache(self):
        if self.cache is None:
            return
        filename = f"sri_node_normalizer_curie_cache.pickle"
        print(f"INFO: Storing SRI normalizer cache to {filename}")
        with open(filename, "wb") as outfile:
            pickle.dump(self.cache, outfile)


    # ############################################################################################
    # Load the cache of all normalizer results
    def load_cache(self):
        filename = f"sri_node_normalizer_curie_cache.pickle"
        if os.path.exists(filename):
            print(f"INFO: Reading SRI normalizer cache from {filename}")
            with open(filename, "rb") as infile:
                self.cache = pickle.load(infile)
        else:
            print(f"INFO: SRI node normalizer cache {filename} does not yet exist. Need to fill it.")


    # ############################################################################################
    # Fill the cache with KG nodes
    def fill_cache(self):

        # Get a hash of curie prefixes supported
        self.get_supported_prefixes()

        filename = 'kg2_node_info.tsv'
        filesize = os.path.getsize(filename)

        # Correction for Windows line endings
        extra_bytes = 0
        fh = open(filename, 'rb')
        if platform.system() == 'Windows' and b'\r\n' in fh.read():
            print('WARNING: Windows line ending requires bytes_read compenstation')
            extra_bytes = 1
        fh.close()

        # Open the file and read in the curies
        fh = open(filename, 'r', encoding="latin-1", errors="replace")
        print(f"INFO: Reading {filename} to pre-fill the normalizer cache")
        previous_percentage = -1
        line_counter = 0
        supported_curies = 0
        bytes_read = 0

        # Create dicts to hold all the information
        batch = []

        # Loop over each line in the file
        for line in fh:
            bytes_read += len(line) + extra_bytes

            match = re.match(r'^\s*$',line)
            if match:
                continue
            columns = line.strip().split("\t")
            node_curie = columns[0]

            curie_prefix = node_curie.split(':')[0]

            # If we use different curie prefixes than the normalizer, need to fix
            normalizer_curie_prefix = curie_prefix
            normalizer_node_curie = node_curie
            if curie_prefix in self.curie_prefix_tx_arax2sri:
                normalizer_curie_prefix = self.curie_prefix_tx_arax2sri[curie_prefix]
                normalizer_node_curie = re.sub(curie_prefix,normalizer_curie_prefix,node_curie)

            # Decide if we want to keep this curie in the batch of things to look up
            # By default, no
            keep = 0
            # If this is a curie prefix that is supported by the normalizer, then yes
            if normalizer_curie_prefix in self.supported_prefixes:
                keep = 1
            # Unless it's already in the cache, then no
            if normalizer_node_curie in self.cache:
                keep = 0
            # Or if we've reached the end of the file, then set keep to 99 and trigger end-of-file processing of the last batch
            if bytes_read + 3 > filesize and len(batch) > 0:
                keep = 99

            # If we want to put this curie in the batch, or drain the batch at end-of-file
            if keep:

                if keep == 1:
                    supported_curies += 1
                    batch.append(normalizer_node_curie)

                if len(batch) > 500 or keep == 99:
                    if bytes_read + 3 > filesize:
                        print("Drain final batch")
                    results = self.get_node_normalizer_results(batch)
                    print(".", end='', flush=True)
                    for curie in batch:
                        if curie in self.cache:
                            continue
                        curie_prefix = curie.split(':')[0]
                        if curie_prefix not in self.stats:
                            self.stats[curie_prefix] = { 'found': 0, 'not found': 0, 'total': 0 }
                        if results is None or curie not in results or results[curie] is  None:
                            self.cache[curie] = None
                            self.stats[curie_prefix]['not found'] += 1
                        else:
                            self.cache[curie] = results[curie]
                            self.stats[curie_prefix]['found'] += 1
                        self.stats[curie_prefix]['total'] += 1

                    # Clear the batch list
                    batch = []

            # Print out some progress information
            line_counter += 1
            percentage = int(bytes_read*100.0/filesize)
            if percentage > previous_percentage:
                previous_percentage = percentage
                print(str(percentage)+"%..", end='', flush=True)

        # Close and summarize
        fh.close()
        print("")
        print(f"{line_counter} lines read")
        print(f"{bytes_read} bytes read of {filesize} bytes in file")
        print(f"{supported_curies} curies with prefixes supported by the SRI normalizer")

        print("Build stats:")
        print(json.dumps(self.stats, indent=2, sort_keys=True))

        # Store or sync the results
        self.store_cache()


    # ############################################################################################
    # Retrieve the dict of supported BioLink types
    def get_supported_types(self):

        # If we've already done this before, return the cached result
        if self.supported_types is not None:
            return self.supported_types

        # Build the URL and fetch the result
        url = f"{BASE_URL}/get_semantic_types"

        with open("semantic_types.json", "r") as f:
         response_content = json.load(f)
        
        # response_content = requests.get(url, headers={'accept': 'application/json'})
        # status_code = response_content.status_code

        # # Check for a returned error
        # if status_code != 200:
        #     eprint(f"ERROR returned with status {status_code} while retrieving supported types")
        #     eprint(response_content)
        #     exit(211)

        # # Unpack the response into a dict and return it
        # response_dict = response_content.json()

        if 'semantic_types' not in response_content:
            eprint(f"ERROR Did not find expected 'semantic_types'")
            return
        if 'types' not in response_content['semantic_types']:
            eprint(f"ERROR Did not find expected 'types' list")
            return

        node_types = {}
        for node_type in response_content['semantic_types']['types']:
            node_types[node_type] = 1

        if len(node_types) == 0:
            node_types = None

        # Save this for future use
        self.supported_types = node_types

        return node_types


    # ############################################################################################
    # Retrieve the dict of supported curie prefixes
    def get_supported_prefixes(self):

        # If we've already done this before, return the cached result
        if self.supported_prefixes is not None:
            return self.supported_prefixes
        supported_prefixes = {}
        with open("curie_prefix.json", "r"):
            response_content = json.load(f)

        # Build the URL and fetch the result
        # url = f"{BASE_URL}/get_curie_prefixes"
        # response_content = requests.get(url, headers={'accept': 'application/json'})
        # status_code = response_content.status_code

        # # Check for a returned error
        # if status_code != 200:
        #     eprint(f"ERROR returned with status {status_code} while retrieving supported types")
        #     eprint(response_content)
        #     exit(253)

        # # Unpack the response into a dict and return it
        # response_dict = response_content.json()
        for entity_name,entity in response_content.items():
            if 'curie_prefix' not in entity:
                eprint(f"ERROR Did not find expected 'curie_prefix'")
                return
            for curie_prefix,count in entity['curie_prefix'].items():
                if str(count) != 'Not found':
                    supported_prefixes[curie_prefix] = count

        # Save this for future use
        self.supported_prefixes = supported_prefixes
        return supported_prefixes


    # ############################################################################################
    # Directly fetch a normalization for a CURIE from the Normalizer
    def get_node_normalizer_results(self, curies, cache_only=None):

        if isinstance(curies,str):
            #print(f"INFO: Looking for curie {curies}")
            if curies in self.cache:
                #print(f"INFO: Using prefill cache for lookup on {curies}")
                result = { curies: self.cache[curies] }
                return result
            curies = [ curies ]

        if cache_only is not None:
            print(f"ERROR: Call to sri_node_normalizer requested cache_only and we missed the cache with {curies}")

        # Build the URL and fetch the result
        url = f"{BASE_URL}/get_normalized_nodes"

        #eprint(f"{len(url)},")

        retry = 0
        sleep_time = 5
        error_state = True

        while retry < 3 and error_state:

            error_state = False

            try:
                response_content = requests.post(url, json={"curies": curies}, headers={'accept': 'application/json'})
            except:
                print("Uncaught error during web request to SRI normalizer")
                error_state = True
                status_code = 0
                response_content = "uncaught error - no response content"
            else:
                status_code = response_content.status_code

            # Check for a returned error
            if status_code == 404:
                #eprint(f"INFO: No normalization data for {curie}")
                return
            elif status_code != 200:
                eprint(f"ERROR returned with status {status_code} while searching with curie list of length {len(curies)} including {curies[0]}")
                eprint(response_content)
                error_state = True

            if error_state:
                print(f"Try again after {sleep_time} seconds")
                time.sleep(sleep_time)
                retry += 1
                sleep_time += 20

        if error_state:
            return

        # Unpack the response into a dict and return it
        response_dict = response_content.json()
        return response_dict


    # ############################################################################################
    # Return a simple dict with the equivalence information and metadata about a CURIE
    def get_empty_equivalence(self, curie=''):

        response = { 'status': 'EMPTY', 'curie': curie, 'preferred_curie': '', 'preferred_curie_name': '',
            'type': '', 'equivalent_identifiers': [], 'equivalent_names': [] }
        return response


    # ############################################################################################
    # Return a simple dict with the equivalence information and metadata about a CURIE
    def get_curie_equivalence(self, curie, cache_only=None):

        response = { 'status': 'ERROR', 'curie': curie, 'preferred_curie': '', 'preferred_curie_name': '',
            'type': '', 'equivalent_identifiers': [], 'equivalent_names': [] }

        # Do a translation for different curie prefixes
        curie_prefix = curie.split(':')[0]
        normalizer_curie = curie
        if curie_prefix in self.curie_prefix_tx_arax2sri:
            normalizer_curie = re.sub(curie_prefix,self.curie_prefix_tx_arax2sri[curie_prefix],curie)

        results = self.get_node_normalizer_results(normalizer_curie, cache_only=cache_only)
        #print(json.dumps(results, indent=2, sort_keys=True))

        if results is None:
            response['status'] = 'no information'
            return response

        # If input CURIE is not the key of the dict, this is highly unexpected
        if normalizer_curie not in results:
            eprint(f"ERROR: Did not find the curie {normalizer_curie} as a key in the results")
            return response

        if results[normalizer_curie] is None:
            response['status'] = 'no information'
            return response

        # If there is no id in the results, this a highly unexpected
        if 'id' not in results[normalizer_curie]:
            eprint(f"ERROR: Did not find 'id' as a key in the results from the SRI normalizer for curie {normalizer_curie}")
            return response

        # If there is a preferred CURIE, store it and its name
        response['preferred_curie'] = results[normalizer_curie]['id']['identifier']
        if 'label' in results[normalizer_curie]['id']:
            response['preferred_curie_name'] = results[normalizer_curie]['id']['label']

        # Translate the id if necessary
        if curie != normalizer_curie:
            response['preferred_curie'] = re.sub(self.curie_prefix_tx_arax2sri[curie_prefix],curie_prefix,results[normalizer_curie]['id']['identifier'])

        # If there is a returned type, store it
        if 'type' in results[normalizer_curie]:
            node_type = results[normalizer_curie]['type'][0]
            response['type'] = node_type

        # If there are additional equivalent identifiers and names, store them
        names = {}
        if 'equivalent_identifiers' in results[normalizer_curie]:
            for equivalence in results[normalizer_curie]['equivalent_identifiers']:
                if 'identifier' in equivalence:
                    id = equivalence['identifier']
                    if curie != normalizer_curie:
                        id = re.sub(self.curie_prefix_tx_arax2sri[curie_prefix],curie_prefix,id)
                    #response['equivalent_identifiers'].append(id)
                    response['equivalent_identifiers'].append(equivalence)
                if 'label' in equivalence:
                    if equivalence['label'] not in names:
                        response['equivalent_names'].append(equivalence['label'])

        response['status'] = 'OK'
        return response


# ############################################################################################
# Command line interface for this class
def main():

    import argparse
    parser = argparse.ArgumentParser(
        description="Interface to the SRI Node Normalizer", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('-b', '--build', action="store_true",
                        help="If set, (re)build the local SRI Node Normalizer cache from scratch", default=False)
    parser.add_argument('-l', '--load', action="store_true",
                        help="If set, load the previously built local SRI Node Normalizer cache", default=False)
    parser.add_argument('-c', '--curie', action="store",
                        help="Specify a curie to look up with the SRI Node Normalizer (e.g., UniProtKB:P01308, ORPHANET:2322, DRUGBANK:DB11655", default=None)
    parser.add_argument('-p', '--prefixes', action="store_true",
                        help="If set, list the SRI Node Normalizer supported prefixes", default=None)
    parser.add_argument('-t', '--types', action="store_true",
                        help="If set, list the SRI Node Normalizer supported types", default=None)
    args = parser.parse_args()

    if not args.build and not args.curie and not args.prefixes and not args.types:
        parser.print_help()
        sys.exit(2)

    normalizer = SriNodeNormalizer()

    if args.prefixes:
        supported_prefixes = normalizer.get_supported_prefixes()
        print(json.dumps(supported_prefixes, indent=2, sort_keys=True))
        return

    if args.types:
        supported_types = normalizer.get_supported_types()
        print(json.dumps(supported_types, indent=2, sort_keys=True))
        return

    if args.build:
        print("INFO: Beginning SRI Node Normalizer cache building process for KG2. Make sure you have a good network connection as this will download ~2 GB of data.")
        print("INFO: Note that requests-cache is used, so the sri_node_normalizer_requests_cache.sqlite file should be deleted first if it might be stale.")
        normalizer.fill_cache()
        normalizer.store_cache()
        print("INFO: Build process complete")
        return

    if args.load:
        normalizer.load_cache()

    curie = 'UniProtKB:P01308'
    if args.curie:
        curie = args.curie

    #print(platform.system())

    print("==========================================================")
    print("Native SRI Node Normalizer results:")
    normalized_results = normalizer.get_node_normalizer_results(curie)
    print(json.dumps(normalized_results, indent=2, sort_keys=True))

    print("==========================================================")
    print("Local more compact and useful formatting:")
    equivalence = normalizer.get_curie_equivalence(curie)
    print(json.dumps(equivalence, indent=2, sort_keys=True))


if __name__ == "__main__": main()




