Source code for schrodinger.application.livedesign.ld_folder_tree
import re
from collections import defaultdict
from . import login
# Define the string path separator that is used in LDClient
# TODO: if it is ever exposed, reference the separator used in LDClient rather
# than defining it separately here (LDIDEAS-2283)
SEP = '__DEBUG__'
ENDPOINT_UNAVAILABLE = 'Endpoint Unavailable'
MODEL_RE = re.compile(r'(?P<assay>.+?)\s+\((?P<endpoint>.*)\)')
[docs]class LDFolderTree:
    """
    A tree structure that holds LD folder/assay hierarchy.
    """
[docs]    def __init__(self, project_id, parent=None):
        """
        :param project_id: LD project ID
        :type project_id: str
        """
        self.project_id = project_id
        self.toplevel_folders = [
            "Computed Properties", "Computational Models", "Experimental Assays"
        ]
        # a dict for keeping track of assays and their endpoints.
        # key: assay name, values: list of endpoint names
        self.endpoints = defaultdict(set)
        # a dict for keeping track of favorite assays and their endpoints.
        # key: assay name, values: list of endpoint names
        self.favorite_endpoints = defaultdict(set)
        _, self.ld_client, _ = login.get_ld_client_and_models() 
[docs]    def fillFolderTree(self):
        """
        Main function to get folders and assays in the same hierarchy as in LD.
        """
        for toplevel_folder in self.toplevel_folders:
            self._fillToplevelItem(toplevel_folder) 
    def _fillToplevelItem(self, toplevel_folder):
        """
        Fills a toplevel folder, such as "Computational Models" or
        "Experimental Assays", in the tree.
        :param toplevel_folder: Name of the toplevel folder
        :type toplevel_folder: str
        """
        folder_tree = self.ld_client.get_folder_tree_data(
            self.project_id, toplevel_folder)
        try:  # 7.5.1 and up
            data_tree = folder_tree[0]
        except KeyError:  # 7.5
            data_tree = folder_tree
        except IndexError:  # empty folder returned from server
            return
        if data_tree:
            self._parseSubfolders(toplevel_folder, data_tree)
    def _parseSubfolders(self, parent_path, data):
        """
        Iteratively adds subfolders to the tree, until reaches assays.
        :param parent_path: The full path to this node
        :type parent_path: str
        :param data: Data from the LD server to parse.
        :type data: json
        """
        folder_name = parent_path.rsplit(SEP, 1)[-1]
        subfolders = self.ld_client.get_subfolders(folder_name, data)
        for subfolder, node in subfolders.items():
            # subfolder could be a folder or an endpoint
            if not node['children']:
                # From LD version 8.0 and up, an original_name field exists that
                # can be used to retrieve the original model name. This avoids
                # the issue of aliased model names that do not follow the
                # standard naming convention. For versions 7.9 and below, the
                # subfolder name is used.
                model_name = node.get('original_name')
                if model_name is None:
                    model_name = subfolder
                assay, endpoint = self._parseModelName(model_name)
                # LD will create an extra subfolder for assays with the same
                # assay name but different endpoint names. In their UI that is
                # necessary because they do not split assay and endpoint into
                # separate nodes in the tree. Since they are split in the GUI,
                # we can get rid of this redundant folder in the path.  (Or in
                # other words, we are already using assays as a folder by
                # collecting all endpoints with the same assay into a single
                # tree entry, so the extra folder provided by LD is
                # unnecessary)
                if assay != folder_name:
                    assay_path = SEP.join([parent_path, assay])
                else:
                    assay_path = parent_path
                self.endpoints[assay_path].add(endpoint)
                if node.get('favorite'):
                    # 'favorite' is only set on endpoints
                    self.favorite_endpoints[assay_path].add(endpoint)
            else:
                subpath = SEP.join([parent_path, subfolder])
                self._parseSubfolders(subpath, node)
    def _parseModelName(self, model_name):
        """
        Given a model name, return the assay and endpoint. E.g.::
            assay_name, endpoint = self._parseModelName('Model (type) prot')
            assay_name -> 'Model'
            endpoint -> 'type'
        In the case of multiple parenthetical segments, assigns the endpoint to
        be everything between the first "(" and the last ")". If no parenthesis
        is found, returns the entire `model_name` string as the assay name and
        an `ENDPOINT_UNAVAILABLE` as the endpoint.
        :param model_name: the full name of the live design endpoint
        :type model_name: str
        :return: the separated and stripped assay and endpoint name
        :rtype: str, str
        """
        model_name = model_name.strip()
        match = MODEL_RE.match(model_name)
        if match is None:
            # If the input model name is not properly formatted, then return the
            # full value as the assay name, and define no endpoint
            return model_name, ENDPOINT_UNAVAILABLE
        return match.group('assay'), match.group('endpoint')