class iHMPSession(object):
    """
    The iHMP Session class. This class allows you to connect with an OSDF
    instance and begin analysis of iHMP data. It produces skeletons of all
    objects in the iHMP OSDF database. Each object contains its own save, load,
    delete feature.

    Attributes:
        _single (iHMPSession): The iHMP Session that is currently live. None
        otherwise.
    """

    check_python_version(name="Cutlass")

    _single = None

    def __init__(self, username, password, server="osdf.ihmpdcc.org", port=8123,
                 ssl=True):
        """
        The initialization of the iHMPSession for the user.

        Args:
            username (str): The username for OSDF access.
            password (str): The password for OSDF access.
            server (str): The server domain name containing the OSDF instance.
                          Defaults to 'osdf.ihmpdcc.org'.
            port (int): The port allowing access to the OSDF instance.
            ssl (bool): Whether the OSDF server is behind SSL/TLS or not.
                        Defaults to true.
        """
        self._username = username
        self._password = password
        self._server = server
        self._port = port
        self._ssl = ssl
        self._osdf = OSDF(self._server, self._username, self._password,
                          port=self._port, ssl=self._ssl)

        self.logger = logging.getLogger(self.__module__ + '.' + \
                                        self.__class__.__name__)

        if iHMPSession._single is None:
            iHMPSession._single = self

        self.logger.info("Using SSL encryption? %s", str(self._ssl))

    def _get_cutlass_instance(self, name):
        self.logger.debug("In _get_cutlass_instance.")

        classes = {
            "16s_dna_prep"                       : "SixteenSDnaPrep",
            "16s_raw_seq_set"                    : "SixteenSRawSeqSet",
            "16s_trimmed_seq_set"                : "SixteenSTrimmedSeqSet",
            "abundance_matrix"                   : "AbundanceMatrix",
            "annotation"                         : "Annotation",
            "clustered_seq_set"                  : "ClusteredSeqSet",
            "cytokine"                           : "Cytokine",
            "host_assay_prep"                    : "HostAssayPrep",
            "host_epigenetics_raw_seq_set"       : "HostEpigeneticsRawSeqSet",
            "host_seq_prep"                      : "HostSeqPrep",
            "host_transcriptomics_raw_seq_set"   : "HostTranscriptomicsRawSeqSet",
            "host_variant_call"                  : "HostVariantCall",
            "host_wgs_raw_seq_set"               : "HostWgsRawSeqSet",
            "lipidome"                           : "Lipidome",
            "metabolome"                         : "Metabolome",
            "microbiome_assay_prep"              : "MicrobiomeAssayPrep",
            "microb_transcriptomics_raw_seq_set" : "MicrobTranscriptomicsRawSeqSet",
            "project"                            : "Project",
            "proteome"                           : "Proteome",
            "proteome_nonpride"                  : "ProteomeNonPride",
            "sample"                             : "Sample",
            "sample_attr"                        : "SampleAttribute",
            "serology"                           : "Serology",
            "study"                              : "Study",
            "subject"                            : "Subject",
            "subject_attr"                       : "SubjectAttribute",
            "viral_seq_set"                      : "ViralSeqSet",
            "visit"                              : "Visit",
            "visit_attr"                         : "VisitAttribute",
            "wgs_assembled_seq_set"              : "WgsAssembledSeqSet",
            "wgs_raw_seq_set"                    : "WgsRawSeqSet",
            "wgs_dna_prep"                       : "WgsDnaPrep"
        }

        class_name = None
        valid = False

        if name in classes:
            valid = True
            class_name = classes[name]

        instance = None

        if valid:
            module = importlib.import_module("cutlass", package="cutlass")

            class_var = getattr(module, class_name)
            instance = class_var()
        else:
            raise TypeError("%s not defined in %s" % (name, self.__class__))

        return instance

    def __getattr__(self, name):
        if name.startswith("create_"):
            class_lower = name[7:]

        try:
            instance = self._get_cutlass_instance(class_lower)
        except TypeError:
            raise AttributeError("%s not defined in %s" % (name, self.__class__))

        return instance

    @staticmethod
    def get_session():
        """
        Returns the current iHMP session if any is instantiated.

        Args:
            None

        Returns:
            The current iHMP session.
        """
        if iHMPSession._single is None:
            raise Exception("A session has not yet been created.")

        return iHMPSession._single

    def get_osdf(self):
        """
        Returns the OSDF object with access to the OSDF instance.

        Args:
            None

        Returns:
            An OSDF object.
        """
        self.logger.debug("In get_osdf.")
        return self._osdf

    def create_object(self, node_type):
        """
        Returns an empty object of the node_type provided. It must be a
        valid object, or else an exception is raised.

        Args:
            node_type (str): The type of object desired.

        Returns:
            A object of the specified type.
        """
        self.logger.debug("In create_object. Type: %s", node_type)

        instance = None

        try:
            instance = self._get_cutlass_instance(node_type)
        except TypeError:
            raise ValueError("Invalid node type specified: %s" % node_type)

        return instance

    @property
    def password(self):
        """
        str: The password provided by the user for the OSDF instance.
        """
        self.logger.debug("In 'password' getter.")
        return self._password

    @password.setter
    @enforce_string
    def password(self, password):
        """
        The password setter

        Args:
            password (str): The new password for OSDF access.

        Returns:
            None
        """
        self.logger.debug("In password setter.")
        self._password = password

        # Ensure the OSDF object gets the new connection parameter
        self.logger.debug("Setting the password in the OSDF client.")
        self._osdf.password = password

    @property
    def port(self):
        """
        int: The port for access to the OSDF instance on the server.
        """
        self.logger.debug("In 'port' getter.")
        return self._port

    @port.setter
    @enforce_int
    def port(self, port):
        """
        The port setter.

        Args:
            port (int): The new port for OSDF access.

        Returns:
            None
        """
        self.logger.debug("In 'port' setter.")
        self._port = port

        # Ensure the OSDF object gets the new connection parameter
        self.logger.debug("Setting the port in the OSDF client.")
        self._osdf.port = port

    @property
    def server(self):
        """
        str: The server domain name that contains the live OSDF instance.
        """
        self.logger.debug("In 'server' getter.")
        return self._server

    @server.setter
    @enforce_string
    def server(self, server):
        """
        The server setter.

        Args:
            server (str): The new server for OSDF access.

        Returns:
            None
        """
        self.logger.debug("In 'server' setter.")
        self._server = server

        # Ensure the OSDF object gets the new connection parameter
        self.logger.debug("Setting the server in the OSDF client.")
        self._osdf.server = server

    @property
    def ssl(self):
        """
        str: Retrieve whether the session is using SSL or not.
        """
        self.logger.debug("In 'ssl' getter.")
        return self._ssl

    @ssl.setter
    @enforce_bool
    def ssl(self, ssl):
        """
        The ssl setter.

        Args:
            ssl (bool): Whether to use encryption (SSL) or not.

        Returns:
            None
        """
        self.logger.debug("In 'ssl' setter.")
        self._ssl = ssl

        # Ensure the OSDF object gets the new connection parameter
        self.logger.debug("Setting the SSL flag in the OSDF client.")
        self._osdf.ssl = ssl

    @property
    def username(self):
        """
        str: The username for access to the OSDF server.
        """
        self.logger.debug("In 'username' getter.")
        return self._username

    @username.setter
    @enforce_string
    def username(self, username):
        """
        The username setter.

        Args:
            username (str): The new username for OSDF access.

        Returns:
            None
        """
        self.logger.debug("In 'username' setter.")
        self._username = username

        # Ensure the OSDF object gets the new connection parameter
        self.logger.debug("Setting the username in the OSDF client.")
        self._osdf.username = username class MIXS(object):
    """
    The MIXS class. This class contains all required fields, as well as validation
    of the passed in dictionary to ensure all fields are there.

    Attributes:
        fields (dict): The required fields and their specific type.
    """
    _fields = {
        "biome": str,
        "body_product": str,
        "collection_date": str,
        "env_package": str,
        "feature": str,
        "geo_loc_name": str,
        "lat_lon": str,
        "material": str,
        "project_name": str,
        "rel_to_oxygen": str,
        "samp_collect_device": str,
        "samp_mat_process": str,
        "samp_size": str,
        "source_mat_id": list
    }

    @staticmethod
    def required_fields():
        """
        A static method. The required fields for the class.

        Args:
            None
        Returns:
            Tuple containing the required fields.
        """
        return tuple(MIXS._fields.keys())

    @staticmethod
    def check_dict(candidate):
        """
        A static method. Validates to ensure that the provided
        candidate dictionary contains all the necessary fields to
        save and ensure data validation when inserted to the OSDF
        instance.

        Args:
            candidate (dict): The possible MIXS dictionary passed in.
        Returns:
            True if the candidate is valid, False otherwise.
        """
        valid = True

        for mixs_key in MIXS.required_fields():
            if mixs_key not in candidate:
                valid = False
                module_logger.error("MIXS field %s is not present." % mixs_key)
                break

        if valid:
            for candidate_key in candidate:
                if candidate_key not in MIXS.required_fields():
                    module_logger.error("Provided MIXS field %s is not a valid field name." % candidate_key)
                    valid = False
                    break

                # The provided key IS in the MIXS table, now we
                # need to check the type...
                if not isinstance(candidate[candidate_key], MIXS._fields[candidate_key]):
                    module_logger.error("Provided MIXS field of %s is not the right type!" % candidate_key)
                    valid = False
                    break

        return valid class MixsException(Exception):
    """
    Exception for the MIXS

    Attributes:
        key (str): The key for the exception.
        message (str): The error message to provide for when the exception is thrown.
    """
    def __init__(self, message, key=None):
        self.key = key
        self.message = message

    def __str__(self):
        if self.key is not None:
            return "Error [%s]: %s" % (self.key, self.message)
        else:
            return "Error: %s" % self.message class Base(object):
    """
    The parent class from which all objects inherit specific features from. This class
    contains all the fields required to all sub-classes (ID, version, links, and tags).

    Attributes:
        namespace (str): The namespace this class will use in the OSDF instance
    """
    namespace = "ihmp"

    def __init__(self):
        """
        Constructor for the Base class. This should not be called from the user, so the
        user should instantiate an instance of the Base class. This initializes the
        OSDF ID, Version, Links, and Tags for all sub-classes

        Args:
            None
        """
        self.logger = logging.getLogger(self.__module__ + '.' + self.__class__.__name__)

        self.logger.addHandler(logging.NullHandler())

        self._id = None
        self._version = None
        self._links = {}
        self._tags = []

    @property
    def id(self):
        """
        str: An alphanumeric indicating the specific ID assigned to the document
             in OSDF.
        """
        self.logger.debug("In 'id' getter.")
        return self._id

    def _set_id(self, node_id):
        """
        The setter for the OSDF ID. This is a private method, so the user should not
        call this. In property form.

        Args:
            node_id (str): The new node ID to assign to this instance

        Returns:
            None
        """
        self.logger.debug("In private _set_id.")
        self._id = node_id

    @property
    def version(self):
        """
        int: The specific version of this instance in the OSDF instance.
             The version must be of type integer, and must be a number
             greater than 0
        """
        self.logger.debug("In 'version' getter.")
        return self._version

    @version.setter
    @enforce_int
    def version(self, version):
        self.logger.debug("In 'version' setter.")

        if version <= 0:
            raise ValueError("Invalid version. Must be a postive integer.")

        self._version = version

    @property
    def links(self):
        """
        Dictionary[str]: The dictionary of links between the current instance
                         and another instance. The link must follow the key-value
                         pair specified by the JSON schema indicating links in the
                         OSDF instance.
        """
        self.logger.debug("In 'links' getter.")
        return self._links

    @links.setter
    @enforce_dict
    def links(self, links):
        self.logger.debug("In 'links' setter.")

        for (link, node_list) in links.items():
            if (type(link) != str) or (type(node_list) != list):
                raise ValueError("Links must be a dict of strings to lists.")

        self._links = links

    @property
    def tags(self):
        """
        List[str]: The list of tags for the instance. This is a required field for
                   all OSDF objects. The tag provided must be of type list. If not,
                   a ValueError is raised indicating the tags must be of type list.
        """
        self.logger.debug("In 'tags' getter.")
        return self._tags

    @tags.setter
    @enforce_list
    def tags(self, tags):
        self.logger.debug("In 'tags' setter.")

        self._tags = tags

    def add_tag(self, tag):
        """
        Adds a new tag to the current tags list for the instance.
        The method checks to make sure that the tag is not already
        present in the current tags list.

        Args:
            tag (str): The new tag to add

        Returns:
            None

        Exceptions:
            ValueError exception if the tag is already present
        """
        self.logger.debug("In add_tag. New tag: %s", tag)
        if tag not in self._tags:
            self._tags.append(tag)
        else:
            raise ValueError("Tag already present for this subject")

    def validate(self):
        """
        Validates the current object's data/JSON against the current
        schema in the OSDF instance for that specific object. All required
        fields for that specific object must be present.

        Args:
            None

        Returns:
            A list of strings, where each string is the error that the
            validation raised during OSDF validation
        """
        self.logger.debug("In validate.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        (valid, error_message) = session.get_osdf().validate_node(document)

        problems = []
        if not valid:
            self.logger.info("Validation did not succeed.")
            problems.append(error_message)

        self.logger.debug("Number of validation problems: %s.", str(len(problems)))
        return problems

    def is_valid(self):
        """
        Validates the current object's data/JSON against the current schema
        in the OSDF instance for the specific object. However, unlike
        validates(), this method does not provide exact error messages,
        it states if the validation was successful or not.

        Args:
            None

        Returns:
            True if the data validates, False if the current state of
            fields in the instance do not validate with the OSDF instance
        """
        self.logger.debug("In is_valid.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        (valid, _error_message) = session.get_osdf().validate_node(document)

        self.logger.debug("Valid? %s", str(valid))

        return valid

    def to_json(self, indent=4):
        """
        Converts the current object from a raw dictionary to a pretty-printed
        JSON string.

        Args:
            indent (int): The indent used to pretty print the JSON string

        Returns:
            A JSON string with all fields/properties of the current instance
        """
        self.logger.debug("In to_json.")

        doc = self._get_raw_doc()

        self.logger.debug("Encoding structure to JSON.")

        json_str = json.dumps(doc, indent=indent)

        self.logger.debug("Dump to JSON successful. Length: %s characters", str(len(json_str)))

        return json_str

    def search(self, query):
        """
        Searches the OSDF instance using the specified input parameters

        Args:

        Returns:

        """
        self.logger.debug("In search.")

        session = iHMPSession.get_session()

        self.logger.info("Got iHMP session.")

    def delete(self):
        """
        Deletes the current object. The object must already have been saved/present
        in the OSDF instance, so an ID for the object must have been already set.

        Args:
            None

        Returns:
            True if the object was successfully deleted, False otherwise

        Exceptions:
            Exception: If the instance does not have an ID set (was never saved in OSDF)
        """
        self.logger.debug("In delete.")

        if self._id is None:
            self.logger.warn("Attempt to delete a node with no ID.")
            raise Exception("Node does not have an ID.")

        visit_node_id = self._id

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        # Assume failure
        success = False

        try:
            self.logger.info("Deleting node with OSDF ID %s.", visit_node_id)
            session.get_osdf().delete_node(visit_node_id)
            success = True
        except Exception as delete_exception:
            self.logger.exception(delete_exception)
            self.logger.error("An error occurred when deleting %s.", self)

        return success

    def children(self, flatten=False):
        """ Returns the children of this node. """
        self.logger.debug("In children.")

        # local imports to avoid cyclic imports
        from .dependency import dependency_methods
        from .dependency import generator_flatten

        def _children(obj):
            yield obj
            name = obj.__class__.__name__
            dep_method = getattr(obj, dependency_methods.get(name, ''), None)
            if dep_method:
                for child in dep_method():
                    yield _children(child)
        cs = islice(_children(self), 1, None)
        if flatten:
            return generator_flatten(cs)
        else:
            return cs

    def __call__(self):
        return self

    def __str__(self):
        _id = "no ID" if not self._id else self._id[-8:]
        name = None

        for attr in ("_name", "_rand_subject_id", "_date", "_local_file"):
            if hasattr(self, attr):
                name = getattr(self, attr)
                break

        if name:
            return "<{} `{}' ({})>".format(self.__class__.__name__, name, _id)
        else:
            return "<{} ({})>".format(self.__class__.__name__, _id)


    __repr__ = __str__

    def __hash__(self):
        if not self._id:
            raise TypeError("Unhashable; must have ID: '{}')".format(str(self)))

        return hash(self._id)

    def __eq__(self, other):
        if self._id and other._id:
            return self._id == other._id

        return False class WgsDnaPrep(Base):
    """
    The class encapsulates an iHMP WGS DNA Prep for iHMP.
    The class contains all the fields required to save a WGS DNA Prep in
    OSDF.

    Attributes:
        namespace (str): The namespace this class will use in OSDF.
    """
    namespace = "ihmp"

    def __init__(self, *args, **kwargs):
        """
        Constructor for the WgsDnaPrep class. This initializes the fields specific
        to the WgsDnaPrep class, and inherits from the Base class.

        Args:
            None
        """
        self.logger = logging.getLogger(self.__module__ + '.' + self.__class__.__name__)

        self.logger.addHandler(logging.NullHandler())

        self._id = None
        self._version = None
        self._links = {}
        self._tags = []

        self._comment = None
        self._frag_size = None
        self._lib_layout = None
        self._lib_selection = None
        self._mims = None
        self._ncbi_taxon_id = None
        self._prep_id = None
        self._sequencing_center = None
        self._sequencing_contact = None
        self._srs_id = None
        self._storage_duration = None

        super(WgsDnaPrep, self).__init__(*args, **kwargs)

    def validate(self):
        """
        Validates the current object's data/JSON against the current
        schema in the OSDF instance for that specific object. All required
        fields for that specific object must be present.

        Args:
            None

        Returns:
            A list of strings, where each string is the error that the
            validation raised during OSDF validation
        """
        self.logger.debug("In validate.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        (valid, error_message) = session.get_osdf().validate_node(document)

        problems = []

        if not valid:
            self.logger.info("Validation did not succeed for %s.", __name__)
            problems.append(error_message)

        if 'prepared_from' not in self._links.keys():
            problems.append("Must add a 'prepared_from' link to a sample.")

        self.logger.debug("Number of validation problems: %s.", len(problems))
        return problems

    def is_valid(self):
        """
        Validates the current object's data/JSON against the current schema
        in the OSDF instance for the specific object. However, unlike
        validates(), this method does not provide exact error messages,
        it states if the validation was successful or not.

        Args:
            None

        Returns:
            True if the data validates, False if the current state of
            fields in the instance do not validate with the OSDF instance
        """
        self.logger.debug("In is_valid.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        (valid, _error_message) = session.get_osdf().validate_node(document)

        if 'prepared_from' not in self._links.keys():
            self.logger.error("Must have of 'prepared_from' linkage.")
            valid = False

        self.logger.debug("Valid? %s", str(valid))

        return valid

    @property
    def comment(self):
        """
        str: Free-text comment.
        """
        self.logger.debug("In 'comment' getter.")

        return self._comment

    @comment.setter
    @enforce_string
    def comment(self, comment):
        """
        The setter for the comment. The comment must be a string.

        Args:
            comment (str): The new comment to add to the string.

        Returns:
            None
        """
        self.logger.debug("In 'comment' setter.")

        self._comment = comment

    @property
    def frag_size(self):
        """
        int: Target library fragment size after shearing.
        """
        self.logger.debug("In 'frag_size' getter.")

        return self._frag_size

    @frag_size.setter
    @enforce_int
    def frag_size(self, frag_size):
        """
        The setter for the WgsDnaPrep fragment size. The size must be an
        integer, and greater than 0.

        Args:
            frag_size (int): The new fragment size.

        Returns:
            None
        """
        self.logger.debug("In 'frag_size' setter.")

        if frag_size < 0:
            raise ValueError("Invalid frag_size. Must be non-negative.")

        self._frag_size = frag_size

    @property
    def lib_layout(self):
        """
        str: Specification of the layout: fragment/paired, and if paired,
             then nominal insert size and standard deviation.
        """
        self.logger.debug("In 'lib_layout' getter.")

        return self._lib_layout

    @lib_layout.setter
    @enforce_string
    def lib_layout(self, lib_layout):
        """
        The setter for the lib layout.

        Args:
            lib_layout (str): The new lib layout.

        Returns:
            None
        """
        self.logger.debug("In 'lib_layout' setter.")

        self._lib_layout = lib_layout

    @property
    def lib_selection(self):
        """
        str: A controlled vocabulary of terms describing selection or reduction
             method used in library construction. Terms used by TCGA include
             (random, hybrid selection)
        """
        self.logger.debug("In 'lib_selection' getter.")

        return self._lib_selection

    @lib_selection.setter
    @enforce_string
    def lib_selection(self, lib_selection):
        """
        The setter for the WgsDnaPrep lib selection.

        Args:
            lib_selection (str): The new lib selection.

        Returns:
            None
        """
        self.logger.debug("In 'lib_selection' setter.")

        self._lib_selection = lib_selection

    @property
    def mims(self):
        """
        mimarks: Genomic Standards Consortium MIMS fields.
        """
        self.logger.debug("In mims getter.")

        return self._mims

    @mims.setter
    @enforce_dict
    def mims(self, mims):
        """
        The setter for the WgsDnaPrep MIMS. The provided dictionary
        must validate based on the MIMS class.

        Args:
            mims (dict): A MIMS dictionary.

        Returns:
            None
        """
        self.logger.debug("In mims setter.")

        valid_dictionary = MIMS.check_dict(mims)

        # Validate the incoming MIMS data
        if valid_dictionary:
            self.logger.debug("MIMS data seems correct.")
            self._mims = mims
        else:
            raise MimsException("Invalid MIMS data detected.")

    @property
    def ncbi_taxon_id(self):
        """
        str: NCBI taxon id.
        """
        self.logger.debug("In 'ncbi_taxon_id' getter.")

        return self._ncbi_taxon_id

    @ncbi_taxon_id.setter
    @enforce_string
    def ncbi_taxon_id(self, ncbi_taxon_id):
        """
        The setter for the WgsDnaPrep NCBI Taxon ID.

        Args:
            ncbi_taxon_id (str): The new NCBI Taxon ID.

        Returns:
            None
        """
        self.logger.debug("In 'ncbi_taxon_id' setter.")

        self._ncbi_taxon_id = ncbi_taxon_id

    @property
    def prep_id(self):
        """
        str: Nucleic Acid Prep ID.
        """
        self.logger.debug("In 'prep_id' getter.")

        return self._prep_id

    @prep_id.setter
    @enforce_string
    def prep_id(self, prep_id):
        """
        The setter for the WgsDnaPrep Prep ID.

        Args:
            prep_id (str): The new Prep ID.

        Returns:
            None
        """
        self.logger.debug("In 'prep_id' setter.")

        self._prep_id = prep_id

    @property
    def sequencing_center(self):
        """
        str: The center responsible for generating the 16S DNA Prep.
        """
        self.logger.debug("In 'sequencing_center' getter.")

        return self._sequencing_center

    @sequencing_center.setter
    @enforce_string
    def sequencing_center(self, sequencing_center):
        """
        The setter for the WgsDnaPrep sequencing center.

        Args:
            sequencing_center (str): The new sequencing center.

        Returns:
            None
        """
        self.logger.debug("In 'sequencing_center' setter.")

        self._sequencing_center = sequencing_center

    @property
    def sequencing_contact(self):
        """
        str: Name and email of the primary contact at the sequencing center.
        """
        self.logger.debug("In 'sequencing_contact' getter.")

        return self._sequencing_contact

    @sequencing_contact.setter
    @enforce_string
    def sequencing_contact(self, sequencing_contact):
        """
        The setter for the WgsDnaPrep sequencing_contact.

        Args:
            sequencing_contact (str): The new sequencing_contact.

        Returns:
            None
        """
        self.logger.debug("In 'sequencing_contact' setter.")

        self._sequencing_contact = sequencing_contact

    @property
    def storage_duration(self):
        """
        int: Duration for which sample was stored in days.
        """
        self.logger.debug("In 'storage_duration' getter.")

        return self._storage_duration

    @storage_duration.setter
    @enforce_int
    def storage_duration(self, storage_duration):
        """
        The setter for the WgsDnaPrep storage duration. The duration must
        be an integer, and greater than 0.

        Args:
            storage_duration (int): The new storage duration.

        Returns:
            None
        """
        self.logger.debug("In 'storage_duration' setter.")

        if storage_duration < 0:
            raise ValueError("Invalid storage_duration. Must be non-negative.")

        self._storage_duration = storage_duration

    @property
    def srs_id(self):
        """
        str: NCBI Sequence Read Archive sample ID of the form SRS012345.
        """
        self.logger.debug("In 'srs_id' getter.")

        return self._srs_id

    @srs_id.setter
    @enforce_string
    def srs_id(self, srs_id):
        """
        The setter for the WgsDnaPrep SRS ID. The ID must be a string, and
        greater than 3 characters long.

        Args:
            srs_id (str): The new SRS ID.

        Returns:
            None
        """
        self.logger.debug("In 'srs_id' setter.")

        if len(srs_id) < 3:
            raise Exception("SRS ID is too short, must be more than 3 characters.")

        self._srs_id = srs_id

    @staticmethod
    def required_fields():
        """
        A static method. The required fields for the class.

        Args:
            None
        Returns:
            Tuple of strings of required properties.
        """
        module_logger.debug("In required_fields.")
        return ("comment", "lib_layout", "lib_selection", "mims",
                "ncbi_taxon_id", "prep_id", "sequencing_center",
                "sequencing_contact", "storage_duration", "tags")

    def _get_raw_doc(self):
        """
        Generates the raw JSON document for the current object. All required fields are
        filled into the JSON document, regardless they are set or not. Any remaining
        fields are included only if they are set. This allows the user to visualize
        the JSON to ensure fields are set appropriately before saving into the
        database.

        Args:
            None

        Returns:
            A dictionary representation of the JSON document.
        """
        self.logger.debug("In _get_raw_doc.")

        wgs_doc = {
            'acl': {
                'read': ['all'],
                'write': [WgsDnaPrep.namespace]
            },
            'linkage': self._links,
            'ns': WgsDnaPrep.namespace,
            'node_type': 'wgs_dna_prep',
            'meta': {
                'comment': self._comment,
                'lib_layout': self._lib_layout,
                'lib_selection': self._lib_selection,
                'mims': self._mims,
                'ncbi_taxon_id': self._ncbi_taxon_id,
                'prep_id': self._prep_id,
                'sequencing_center': self._sequencing_center,
                'sequencing_contact': self._sequencing_contact,
                'storage_duration': self._storage_duration,
                'subtype': "wgs",
                'tags': self._tags
            }
        }

        if self._id is not None:
            self.logger.debug("%s object has the OSDF id set.", __name__)
            wgs_doc['id'] = self._id

        if self._version is not None:
            self.logger.debug("%s object has the OSDF version set.", __name__)
            wgs_doc['ver'] = self._version

        if self._frag_size is not None:
            self.logger.debug("%s object has the frag_size set.", __name__)
            wgs_doc['meta']['frag_size'] = self._frag_size

        if self._srs_id is not None:
            self.logger.debug("%s object has the srs_id set.", __name__)
            wgs_doc['meta']['srs_id'] = self._srs_id

        return wgs_doc

    @staticmethod
    def search(query="\"wgs_dna_prep\"[node_type]"):
        """
        Searches the OSDF database through all WgsDnaPrep node types. Any
        criteria the user wishes to add is provided by the user in the query
        language specifications provided in the OSDF documentation. A general
        format is (including the quotes and brackets):

        "search criteria"[field to search]

        If there are any results, they are returned as a WgsDnaPrep instance,
        otherwise an empty list will be returned.

        Args:
            query (str): The query for the OSDF framework. Defaults to the
                         WgsDnaPrep node type.

        Returns:
            Returns an array of WgsDnaPrep objects. It returns an empty list if
            there are no results.
        """
        module_logger.debug("In search.")

        session = iHMPSession.get_session()
        module_logger.info("Got iHMP session.")

        if query != '"wgs_dna_prep"[node_type]':
            query = '({}) && "wgs_dna_prep"[node_type]'.format(query)

        module_logger.debug("Submitting OQL query: %s", query)

        wgsDnaPrep_data = session.get_osdf().oql_query(WgsDnaPrep.namespace, query)

        all_results = wgsDnaPrep_data['results']

        result_list = list()

        if len(all_results) > 0:
            for result in all_results:
                prep_result = WgsDnaPrep.load_wgsDnaPrep(result)
                result_list.append(prep_result)

        return result_list

    @staticmethod
    def load_wgsDnaPrep(prep_data):
        """
        Takes the provided JSON string and converts it to a WgsDnaPrep object

        Args:
            prep_data (str): The JSON string to convert

        Returns:
            Returns a WgsDnaPrep instance.
        """
        module_logger.info("Creating a template %s.", __name__)
        prep = WgsDnaPrep()

        module_logger.debug("Filling in %s details.", __name__)

        # The attributes commmon to all iHMP nodes
        prep._set_id(prep_data['id'])
        prep.version = prep_data['ver']
        prep.links = prep_data['linkage']

        # The attributes that are particular to WgsDnaPrep documents
        prep.comment = prep_data['meta']['comment']
        prep.lib_layout = prep_data['meta']['lib_layout']
        prep.lib_selection = prep_data['meta']['lib_selection']
        prep.mims = prep_data['meta']['mims']
        prep.ncbi_taxon_id = prep_data['meta']['ncbi_taxon_id']
        prep.prep_id = prep_data['meta']['prep_id']
        prep.sequencing_center = prep_data['meta']['sequencing_center']
        prep.sequencing_contact = prep_data['meta']['sequencing_contact']
        prep.storage_duration = prep_data['meta']['storage_duration']
        prep.tags = prep_data['meta']['tags']

        if 'frag_size' in prep_data['meta']:
            module_logger.info(__name__ + " data has 'frag_size' present.")
            prep.frag_size = prep_data['meta']['frag_size']

        if 'srs_id' in prep_data['meta']:
            module_logger.info(__name__ + " data has 'srs_id' present.")
            prep.srs_id = prep_data['meta']['srs_id']

        module_logger.debug("Returning loaded %s", __name__)

        return prep

    @staticmethod
    def load(prep_id):
        """
        Loads the data for the specified input ID from the OSDF instance to
        this object.  If the provided ID does not exist, then an error message
        is provided stating the project does not exist.

        Args:
            prep_id (str): The OSDF ID for the document to load.

        Returns:
            A WgsDnaPrep object with all the available OSDF data loaded into it.
        """
        module_logger.debug("In load. Specified ID: %s", prep_id)

        session = iHMPSession.get_session()
        module_logger.info("Got iHMP session.")
        prep_data = session.get_osdf().get_node(prep_id)
        prep = WgsDnaPrep.load_wgsDnaPrep(prep_data)

        module_logger.debug("Returning loaded %s.", __name__)
        return prep

    def save(self):
        """
        Saves the data in the current instance. The JSON form of the current
        data for the instance is validated in the save function. If the data is
        not valid, then the data will not be saved. If the instance was saved
        previously, then the node ID is assigned the alpha numeric found in the
        OSDF instance. If not saved previously, then the node ID is 'None', and
        upon a successful, will be assigned to the alpha numeric ID found in
        OSDF. Also, the version is updated as the data is saved in OSDF.

        Args:
            None

        Returns;
            True if successful, False otherwise.

        """
        self.logger.debug("In save.")

        if not self.is_valid():
            self.logger.error("Cannot save, data is invalid")
            return False

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        success = False

        if self.id is None:
            # The document has not yet been save
            prep_data = self._get_raw_doc()
            self.logger.info("Got the raw JSON document.")

            try:
                self.logger.info("Attempting to save a new node.")
                node_id = session.get_osdf().insert_node(prep_data)
                self.logger.info("Save for %s %s successful.", __name__, node_id)
                self.logger.info("Setting ID for %s %s.", __name__, node_id)
                self._set_id(node_id)
                self._version = 1
                success = True
            except Exception as insert_exception:
                self.logger.error("An error occurred while inserting %s %s."
                                  "Reason: %s", __name__, self._id,
                                  insert_exception
                                 )
        else:
            prep_data = self._get_raw_doc()

            try:
                self.logger.info("Attempting to update %s with ID: %s.",
                                 __name__, self._id
                                )
                session.get_osdf().edit_node(prep_data)
                self.logger.info("Update for %s %s successful.",
                                 __name__, self._id
                                )
                success = True
            except Exception as update_exception:
                self.logger.error(
                    "An error occurred while updating %s %s. Reason: %s",
                    __name__, self._id, update_exception
                )

        return success

    def child_seq_sets(self):
        """
        Return iterator of all sequence sets descended from this prep.
        """
        self.logger.debug("In child_seq_sets.")

        linkage_query = '"{}"[linkage.sequenced_from]'.format(self.id)

        from cutlass.WgsRawSeqSet import WgsRawSeqSet
        from cutlass.MicrobTranscriptomicsRawSeqSet import MicrobTranscriptomicsRawSeqSet
        from cutlass.ViralSeqSet import ViralSeqSet

        query = iHMPSession.get_session().get_osdf().oql_query

        for page_no in count(1):
            res = query(WgsDnaPrep.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                if doc['node_type'] == "wgs_raw_seq_set":
                    yield WgsRawSeqSet.load_wgsRawSeqSet(doc)
                elif doc['node_type'] == "viral_seq_set":
                    yield ViralSeqSet.load_viral_seq_set(doc)
                elif doc['node_type'] == "microb_transcriptomics_raw_seq_set":
                    yield MicrobTranscriptomicsRawSeqSet.load_microb_transcriptomics_raw_seq_set(doc)

            res_count -= len(res['results'])

            if res_count < 1:
                break class SixteenSDnaPrep(Base):
    """
    The class encapsulating the project data for an iHMP instance.
    This class contains all the fields required to save a SixteenSDnaPrep object
    in OSDF.

    Attributes:
        namespace (str): The namespace this class will use in OSDF.
    """
    namespace = "ihmp"

    def __init__(self, *args, **kwargs):
        """
        Constructor for the SixteenSDnaPrep class. This initializes the fields
        specific to the SixteenSDnaPrep class, and inherits from the Base class.

        Args:
            None
        """
        self.logger = logging.getLogger(self.__module__ + '.' + self.__class__.__name__)

        self.logger.addHandler(logging.NullHandler())

        self._id = None
        self._version = None
        self._links = {}
        self._tags = []

        self._comment = None
        self._frag_size = None
        self._lib_layout = None
        self._lib_selection = None
        self._mimarks = None
        self._ncbi_taxon_id = None
        self._prep_id = None
        self._sequencing_center = None
        self._sequencing_contact = None
        self._srs_id = None
        self._storage_duration = None

        super(SixteenSDnaPrep, self).__init__(*args, **kwargs)

    def validate(self):
        """
        Validates the current object's data/JSON against the current schema in
        OSDF for that specific object. All required fields for the object must
        be present.

        Args:
            None

        Returns:
            A list of strings, where each string is the error that the
            validation raised during OSDF validation.
        """
        self.logger.debug("In validate.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        (valid, error_message) = session.get_osdf().validate_node(document)

        problems = []

        if not valid:
            self.logger.info("Validation did not succeed for %s.", __name__)
            problems.append(error_message)

        if 'prepared_from' not in self._links.keys():
            problems.append("Must add a 'prepared_from' link to a sample.")

        self.logger.debug("Number of validation problems: %s.", len(problems))
        return problems

    def is_valid(self):
        """
        Validates the current object's data/JSON against the current schema
        in OSDF for the specific object. However, unlike
        validate(), this method does not provide exact error messages,
        it states if the validation was successful or not.

        Args:
            None

        Returns:
            True if the data validates, False if the current state of fields in
            the instance do not validate with OSDF.
        """
        self.logger.debug("In is_valid.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        (valid, _error_message) = session.get_osdf().validate_node(document)

        if 'prepared_from' not in self._links.keys():
            self.logger.error("Must have of 'prepared_from' linkage.")
            valid = False

        self.logger.debug("Valid? %s", str(valid))

        return valid

    @property
    def comment(self):
        """
        str: Free-text comment.
        """
        self.logger.debug("In 'comment' getter.")

        return self._comment

    @comment.setter
    @enforce_string
    def comment(self, comment):
        """
        The setter for the comment. The comment must be a string.

        Args:
            comment (str): The new comment to add to the string.

        Returns:
            None
        """
        self.logger.debug("In 'comment' setter.")

        self._comment = comment

    @property
    def frag_size(self):
        """
        int: Target library fragment size after shearing.
        """
        self.logger.debug("In 'frag_size' getter.")

        return self._frag_size

    @frag_size.setter
    @enforce_int
    def frag_size(self, frag_size):
        """
        The setter for the SixteenSDnaPrep fragment size. The size must be an
        integer, and greater than 0.

        Args:
            frag_size (int): The new fragment size.

        Returns:
            None
        """
        self.logger.debug("In 'frag_size' setter.")

        if frag_size < 0:
            raise ValueError("Invalid frag_size. Must be non-negative.")

        self._frag_size = frag_size

    @property
    def lib_layout(self):
        """
        str: Specification of the layout: fragment/paired, and if paired,
             then nominal insert size and standard deviation.
        """
        self.logger.debug("In 'lib_layout' getter.")

        return self._lib_layout

    @lib_layout.setter
    @enforce_string
    def lib_layout(self, lib_layout):
        """
        The setter for the SixteenSDnaPrep lib layout.

        Args:
            lib_layout (str): The new lib layout.

        Returns:
            None
        """
        self.logger.debug("In 'lib_layout' setter.")

        self._lib_layout = lib_layout

    @property
    def lib_selection(self):
        """
        str: A controlled vocabulary of terms describing selection or reduction
             method used in library construction. Terms used by TCGA include
             (random, hybrid selection)
        """
        self.logger.debug("In 'lib_selection' getter.")

        return self._lib_selection

    @lib_selection.setter
    @enforce_string
    def lib_selection(self, lib_selection):
        """
        The setter for the SixteenSDnaPrep lib selection.

        Args:
            lib_selection (str): The new lib selection.

        Returns:
            None
        """
        self.logger.debug("In 'lib_selection' setter.")

        self._lib_selection = lib_selection

    @property
    def mimarks(self):
        """
        mimarks: Genomic Standards Consortium MIMARKS fields.
        """
        self.logger.debug("In 'mimarks' getter.")

        return self._mimarks

    @mimarks.setter
    @enforce_dict
    def mimarks(self, mimarks):
        """
        The setter for the MIMARKS data. The provided dictionary
        must validate based on the MIMARKS class.

        Args:
            mimars (dict): The new MIMARKS dictionary.

        Returns:
            None
        """
        self.logger.debug("In 'mimarks' setter.")

        valid_dictionary = MIMARKS.check_dict(mimarks)

        # Validate the incoming MIMARKS data
        if valid_dictionary:
            self.logger.debug("MIMARKS data seems correct.")
            self._mimarks = mimarks
        else:
            raise MimarksException("Invalid MIMARKS data detected.")

    @property
    def ncbi_taxon_id(self):
        """
        str: NCBI taxon id.
        """
        self.logger.debug("In 'ncbi_taxon_id' getter.")

        return self._ncbi_taxon_id

    @ncbi_taxon_id.setter
    @enforce_string
    def ncbi_taxon_id(self, ncbi_taxon_id):
        """
        The setter for the NCBI Taxon ID.

        Args:
            ncbi_taxon_id (str): The new NCBI Taxon ID.

        Returns:
            None
        """
        self.logger.debug("In 'ncbi_taxon_id' setter.")

        self._ncbi_taxon_id = ncbi_taxon_id

    @property
    def prep_id(self):
        """
        str: Nucleic Acid Prep ID.
        """
        self.logger.debug("In 'prep_id' getter.")

        return self._prep_id

    @prep_id.setter
    @enforce_string
    def prep_id(self, prep_id):
        """
        The setter for the prep ID.

        Args:
            prep_id (str): The new prep ID.

        Returns:
            None
        """
        self.logger.debug("In 'prep_id' setter.")

        self._prep_id = prep_id

    @property
    def srs_id(self):
        """
        str: NCBI Sequence Read Archive sample ID of the form SRS012345.
        """
        self.logger.debug("In 'srs_id' getter.")

        return self._srs_id

    @srs_id.setter
    @enforce_string
    def srs_id(self, srs_id):
        """
        The setter for the SixteenSDnaPrep SRS ID. The ID must be a string, and
        greater than 3 characters long.

        Args:
            srs_id (str): The new SRS ID.

        Returns:
            None
        """
        self.logger.debug("In 'srs_id' setter.")

        if len(srs_id) < 3:
            raise Exception("SRS ID is too short, must be more than 3 characters.")

        self._srs_id = srs_id

    @property
    def sequencing_center(self):
        """
        str: The center responsible for generating the 16S DNA Prep.
        """
        self.logger.debug("In 'sequencing_center' getter.")

        return self._sequencing_center

    @sequencing_center.setter
    @enforce_string
    def sequencing_center(self, sequencing_center):
        """
        The setter for the SixteenSDnaPrep sequencing center.

        Args:
            sequencing_center (str): The new sequencing center.

        Returns:
            None
        """
        self.logger.debug("In 'sequencing_center' setter.")

        self._sequencing_center = sequencing_center

    @property
    def sequencing_contact(self):
        """
        str: Name and email of the primary contact at the sequencing center.
        """
        self.logger.debug("In 'sequencing_contact' getter.")

        return self._sequencing_contact

    @sequencing_contact.setter
    @enforce_string
    def sequencing_contact(self, sequencing_contact):
        """
        The setter for the SixteenSDnaPrep sequencing_contact.

        Args:
            sequencing_contact (str): The new sequencing_contact.

        Returns:
            None
        """
        self.logger.debug("In 'sequencing_contact' setter.")

        self._sequencing_contact = sequencing_contact

    @property
    def storage_duration(self):
        """
        int: Duration for which sample was stored in days.
        """
        self.logger.debug("In 'storage_duration' getter.")

        return self._storage_duration

    @storage_duration.setter
    @enforce_int
    def storage_duration(self, storage_duration):
        """
        The setter for the storage duration. The duration must
        be an integer, and greater than 0.

        Args:
            storage_duration (int): The new storage duration.

        Returns:
            None
        """
        self.logger.debug("In 'storage_duration' setter.")

        if storage_duration < 0:
            raise ValueError("Invalid storage_duration. Must be non-negative.")

        self._storage_duration = storage_duration

    @staticmethod
    def required_fields():
        """
        A static method. The required fields for the class.

        Args:
            None
        Returns:
            Tuple of strings of required properties.
        """
        module_logger.debug("In required_fields.")
        return ("comment", "lib_layout", "lib_selection", "mimarks",
                "ncbi_taxon_id", "prep_id", "sequencing_center",
                "sequencing_contact", "storage_duration", "tags")

    def _get_raw_doc(self):
        """
        Generates the raw JSON document for the current object. All required fields are
        filled into the JSON document, regardless they are set or not. Any remaining
        fields are included only if they are set. This allows the user to visualize
        the JSON to ensure fields are set appropriately before saving into the
        database.

        Args:
            None

        Returns:
            A dictionary representation of the JSON document.
        """
        self.logger.debug("In _get_raw_doc.")

        sixteen_s_doc = {
            'acl': {
                'read': ['all'],
                'write': [SixteenSDnaPrep.namespace]
            },
            'linkage': self._links,
            'ns': SixteenSDnaPrep.namespace,
            'node_type': '16s_dna_prep',
            'meta': {
                'comment': self._comment,
                'lib_layout': self._lib_layout,
                'lib_selection': self._lib_selection,
                'mimarks': self._mimarks,
                'ncbi_taxon_id': self._ncbi_taxon_id,
                'prep_id': self._prep_id,
                'sequencing_center': self._sequencing_center,
                'sequencing_contact': self._sequencing_contact,
                'storage_duration': self._storage_duration,
                'subtype': "16s",
                'tags': self._tags
            }
        }

        if self._id is not None:
            self.logger.debug("%s object has the OSDF id set.", __name__)
            sixteen_s_doc['id'] = self._id

        if self._version is not None:
            self.logger.debug("%s object has the OSDF version set.", __name__)
            sixteen_s_doc['ver'] = self._version

        if self._frag_size is not None:
            self.logger.debug("%s object has the frag_size set.", __name__)
            sixteen_s_doc['meta']['frag_size'] = self._frag_size

        if self._srs_id is not None:
            self.logger.debug("%s object has the srs_id set.", __name__)
            sixteen_s_doc['meta']['srs_id'] = self._srs_id

        return sixteen_s_doc

    @staticmethod
    def search(query="\"16s_dna_prep\"[node_type]"):
        """
        Searches the OSDF database through all 16s DNA prep nodes. Any criteria
        the user wishes to add is provided by the user in the query language
        specifications provided in the OSDF documentation. A general format
        is (including the quotes and brackets):

        "search criteria"[field to search]

        If there are any results, they are returned as a Subject instance,
        otherwise an empty list will be returned.

        Args:
            query (str): The query for the OSDF framework. Defaults to the
                         Subject node type.

        Returns:
            Returns an array of Subject objects. It returns an empty list if
            there are no results.
        """
        module_logger.debug("In search.")

        session = iHMPSession.get_session()
        module_logger.info("Got iHMP session.")

        if query != '"16s_dna_prep"[node_type]':
            query = '({}) && "16s_dna_prep"[node_type]'.format(query)

        module_logger.debug("Submitting OQL query: %s", query)

        sixteenSDnaPrep_data = session.get_osdf().oql_query(
            SixteenSDnaPrep.namespace,
            query
        )

        all_results = sixteenSDnaPrep_data['results']

        result_list = list()

        if len(all_results) > 0:
            for result in all_results:
                sixteens_dna_prep_result = SixteenSDnaPrep.load_sixteenSDnaPrep(result)
                result_list.append(sixteens_dna_prep_result)

        return result_list

    @staticmethod
    def load_sixteenSDnaPrep(prep_data):
        """
        Takes the provided JSON string and converts it to a Subject object

        Args:
            subject_data (str): The JSON string to convert

        Returns:
            Returns a Subject instance.
        """
        module_logger.info("Creating a template %s.", __name__)
        prep = SixteenSDnaPrep()

        module_logger.debug("Filling in %s details.", __name__)

        # The attributes commmon to all iHMP nodes
        prep._set_id(prep_data['id'])
        prep.version = prep_data['ver']
        prep.links = prep_data['linkage']

        # The attributes that are particular to SixteenSDnaPrep documents
        prep.comment = prep_data['meta']['comment']
        prep.lib_layout = prep_data['meta']['lib_layout']
        prep.lib_selection = prep_data['meta']['lib_selection']
        prep.mimarks = prep_data['meta']['mimarks']
        prep.ncbi_taxon_id = prep_data['meta']['ncbi_taxon_id']
        prep.prep_id = prep_data['meta']['prep_id']
        prep.sequencing_center = prep_data['meta']['sequencing_center']
        prep.sequencing_contact = prep_data['meta']['sequencing_contact']
        prep.storage_duration = prep_data['meta']['storage_duration']
        prep.tags = prep_data['meta']['tags']

        if 'frag_size' in prep_data['meta']:
            module_logger.info("%s data has 'frag_size' present.", __name__)
            prep.frag_size = prep_data['meta']['frag_size']

        if 'srs_id' in prep_data['meta']:
            module_logger.info(__name__ + " data has 'srs_id' present.")
            prep.srs_id = prep_data['meta']['srs_id']

        module_logger.debug("Returning loaded %s.", __name__)

        return prep

    def delete(self):
        """
        Deletes the current object (self) from the OSDF instance. If the object
        has not been saved previously (node ID is not set), then an error message
        will be logged stating the object was not deleted. If the ID is set, and
        exists in the OSDF instance, then the object will be deleted from the
        OSDF instance, and this object must be re-saved in order to use it again.

        Args:
            None

        Returns:
            True upon successful deletion, False otherwise.
        """
        self.logger.debug("In delete.")

        if self._id is None:
            self.logger.warn("Attempt to delete a %s with no ID.", __name__)
            raise Exception("%s does not have an ID.", __name__)

        prep_id = self._id

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        # Assume failure
        success = False

        try:
            self.logger.info("Deleting %s with OSDF ID %s.", __name__, prep_id)
            session.get_osdf().delete_node(prep_id)
            success = True
        except Exception as e:
            self.logger.exception(e)
            self.logger.error("An error occurred when deleting %s.", self)

        return success

    @staticmethod
    def load(prep_id):
        """
        Loads the data for the specified input ID from the OSDF instance to this object.
        If the provided ID does not exist, then an error message is provided stating the
        project does not exist.

        Args:
            prep_id (str): The OSDF ID for the document to load.

        Returns:
            A SixteenSDnaPrep object with all the available OSDF data loaded into it.
        """
        module_logger.debug("In load. Specified ID: %s.", prep_id)

        session = iHMPSession.get_session()
        module_logger.info("Got iHMP session.")

        prep_data = session.get_osdf().get_node(prep_id)

        module_logger.info("Creating a template %s.", __name__)
        prep = SixteenSDnaPrep()

        module_logger.debug("Filling in %s details.", __name__)

        # The attributes commmon to all iHMP nodes
        prep._set_id(prep_data['id'])
        prep.version = prep_data['ver']
        prep.links = prep_data['linkage']

        # The attributes that are particular to SixteenSDnaPrep documents
        prep.comment = prep_data['meta']['comment']
        prep.lib_layout = prep_data['meta']['lib_layout']
        prep.lib_selection = prep_data['meta']['lib_selection']
        prep.mimarks = prep_data['meta']['mimarks']
        prep.ncbi_taxon_id = prep_data['meta']['ncbi_taxon_id']
        prep.prep_id = prep_data['meta']['prep_id']
        prep.sequencing_center = prep_data['meta']['sequencing_center']
        prep.sequencing_contact = prep_data['meta']['sequencing_contact']
        prep.storage_duration = prep_data['meta']['storage_duration']
        prep.tags = prep_data['meta']['tags']

        if 'frag_size' in prep_data['meta']:
            module_logger.info("%s data has 'frag_size' present.", __name__)
            prep.frag_size = prep_data['meta']['frag_size']

        if 'srs_id' in prep_data['meta']:
            module_logger.info("%s data has 'srs_id' present.", __name__)
            prep.srs_id = prep_data['meta']['srs_id']

        module_logger.debug("Returning loaded %s.", __name__)

        return prep

    def save(self):
        """
        Saves the data in the current instance. The JSON form of the current data
        for the instance is validated in the save function. If the data is not valid,
        then the data will not be saved. If the instance was saved previously, then
        the node ID is assigned the alpha numeric found in the OSDF instance. If not
        saved previously, then the node ID is 'None', and upon a successful, will be
        assigned to the alpha numeric ID found in the OSDF instance. Also, the
        version is updated as the data is saved in the OSDF instance.

        Args:
            None

        Returns;
            True if successful, False otherwise.

        """
        self.logger.debug("In save.")

        if not self.is_valid():
            self.logger.error("Cannot save, data is invalid")
            return False

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        success = False

        if self.id is None:
            # The document has not yet been save
            prep_data = self._get_raw_doc()
            self.logger.info("Got the raw JSON document.")

            try:
                self.logger.info("Attempting to save a new node.")
                node_id = session.get_osdf().insert_node(prep_data)
                self.logger.info("Save for %s %s successful.", __name__, node_id)
                self.logger.info("Setting ID for %s %s.", __name__, node_id)
                self._set_id(node_id)
                self._version = 1
                success = True
            except Exception as save_exception:
                self.logger.exception(save_exception)
                self.logger.error("An error occurred when inserting %s.", self)

        else:
            prep_data = self._get_raw_doc()

            try:
                self.logger.info("Attempting to update %s with ID: %s.", __name__, self._id)
                session.get_osdf().edit_node(prep_data)
                self.logger.info("Update for %s %s successful.", __name__, self._id)
                success = True
            except Exception as edit_exception:
                self.logger.error("An error occurred while updating %s %s. " + \
                                  "Reason: %s", __name__, self._id, edit_exception)

        return success


    def raw_seq_sets(self):
        """
        Return iterator of all raw_seq_sets sequenced from this prep.
        """
        linkage_query = '"{}"[linkage.sequenced_from]'.format(self.id)
        query = iHMPSession.get_session().get_osdf().oql_query

        for page_no in count(1):
            res = query(SixteenSDnaPrep.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield SixteenSRawSeqSet.load_16s_raw_seq_set(doc)

            res_count -= len(res['results'])

            if res_count < 1:
                break class HostSeqPrep(Base):
    """
    The class encapsulates an HostSeqPrep for the iHMP.  The class
    contains all the fields required to save such an object to OSDF.

    Attributes:
        namespace (str): The namespace this class will use in OSDF.
    """
    namespace = "ihmp"

    def __init__(self, *args, **kwargs):
        """
        Constructor for the class. This initializes the fields specific
        to the HostSeqPrep class, and inherits from the Base class.

        Args:
            None
        """
        self.logger = logging.getLogger(self.__module__ + '.' + self.__class__.__name__)

        self.logger.addHandler(logging.NullHandler())

        self._id = None
        self._version = None
        self._links = {}
        self._tags = []

        self._comment = None
        self._lib_layout = None
        self._lib_selection = None
        self._ncbi_taxon_id = None
        self._prep_id = None
        self._sequencing_center = None
        self._sequencing_contact = None
        self._storage_duration = None
        self._urls = ['']

        # Optional properties
        self._frag_size = None
        self._mims = None
        self._srs_id = None
        self._lib_const_meth = None
        self._lib_vector = None
        self._lib_size = None
        self._experimental_factor = None
        self._nucl_acid_amp = None
        self._samp_mat_process = None
        self._nucl_acid_ext = None
        self._adapters = None
        self._findex = None
        self._rindex = None
        self._lib_screen = None

        super(HostSeqPrep, self).__init__(*args, **kwargs)

    def validate(self):
        """
        Validates the current object's data/JSON against the current
        schema in the OSDF instance for that specific object. All required
        fields for that specific object must be present.

        Args:
            None

        Returns:
            A list of strings, where each string is the error that the
            validation raised during OSDF validation.
        """
        self.logger.debug("In validate.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        (valid, error_message) = session.get_osdf().validate_node(document)

        problems = []

        if not valid:
            self.logger.info("Validation did not succeed for %s.", __name__)
            problems.append(error_message)

        if 'prepared_from' not in self._links.keys():
            problems.append("Must add a 'prepared_from' link to a sample.")

        self.logger.debug("Number of validation problems: %s.", len(problems))
        return problems

    def is_valid(self):
        """
        Validates the current object's data/JSON against the current schema
        in the OSDF instance for the specific object. However, unlike
        validates(), this method does not provide exact error messages,
        it states if the validation was successful or not.

        Args:
            None

        Returns:
            True if the data validates, False if the current state of
            fields in the instance do not validate with the OSDF instance
        """
        self.logger.debug("In is_valid.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        # _error_message is intentionally unused
        (valid, _error_message) = session.get_osdf().validate_node(document)

        if 'prepared_from' not in self._links.keys():
            self.logger.error("Must have a 'prepared_from' linkage.")
            valid = False

        self.logger.debug("Valid? %s", str(valid))

        return valid

    @property
    def adapters(self):
        """
        str: Adapters provide priming sequences for both amplification and
        sequencing of the sample-library fragments. Both adapters should be
        reported in uppercase letters.
        """
        self.logger.debug("In 'adapters' getter.")

        return self._comment

    @adapters.setter
    @enforce_string
    def adapters(self, adapters):
        """
        The setter for the adapters to provide priming sequences for both
        amplification and sequencing of the sample-library fragments.
        and less than 512 characters.

        Args:
            adapters (str): The adapters string (in upper case letters)..

        Returns:
            None
        """
        self.logger.debug("In 'adapters' setter.")

        self._adapters = adapters

    @property
    def comment(self):
        """
        str: Free-text comment.
        """
        self.logger.debug("In 'comment' getter.")

        return self._comment

    @comment.setter
    @enforce_string
    def comment(self, comment):
        """
        The setter for the comment. The comment must be a string,
        and less than 512 characters.

        Args:
            comment (str): The new comment to add to the string.

        Returns:
            None
        """
        self.logger.debug("In 'comment' setter.")

        if len(comment) > 512:
            raise Exception("Comment is too long, must be less than 512 characters.")

        self._comment = comment

    @property
    def experimental_factor(self):
        """
        str: Experimental factors are essentially the variable aspects of an
        experiment design which can be used to describe an experiment, or set
        of experiments, in an increasingly detailed manner. This field accepts
        ontology terms from Experimental Factor Ontology (EFO) and/or Ontology
        for Biomedical Investigations (OBI). For a browser of EFO (v 2.43)
        terms, please see http://purl.bioontology.org/ontology/EFO; for a
        browser of OBI (v 2013-10-25) terms please see
        http://purl.bioontology.org/ontology/OBI
        """
        self.logger.debug("In 'experimental_factor' getter.")

        return self._experimental_factor

    @experimental_factor.setter
    @enforce_string
    def experimental_factor(self, experimental_factor):
        """
        The setter for the experimental factor.

        Args:
            experimental_factor (str): The new experimental factor

        Returns:
            None
        """
        self.logger.debug("In 'experimental_factor' setter.")

        self._experimental_factor = experimental_factor

    @property
    def findex(self):
        """
        str: Forward strand molecular barcode, called Multiplex Identifier (MID),
        that is used to specifically tag unique samples in a sequencing run.
        Sequence should be reported in uppercase letters.
        """
        self.logger.debug("In 'findex' getter.")

        return self._findex

    @findex.setter
    @enforce_string
    def findex(self, findex):
        """
        The setter for the forward strand molecular barcode, called Multiplex
        Identifier (MID).

        Args:
            findex (str): The string to set for findex.

        Returns:
            None
        """
        self.logger.debug("In 'findex' setter.")

        self._findex = findex

    @property
    def frag_size(self):
        """
        int: Target library fragment size after shearing.
        """
        self.logger.debug("In 'frag_size' getter.")

        return self._frag_size

    @frag_size.setter
    @enforce_int
    def frag_size(self, frag_size):
        """
        The setter for the fragment size. The size must be an
        integer, and greater than 0.

        Args:
            frag_size (int): The new fragment size.

        Returns:
            None
        """
        self.logger.debug("In 'frag_size' setter.")

        if frag_size < 0:
            raise ValueError("Invalid frag_size. Must be non-negative.")

        self._frag_size = frag_size

    @property
    def lib_const_meth(self):
        """
        str: Library construction method used for clone libraries.
        """
        self.logger.debug("In 'lib_const_meth' getter.")

        return self._lib_const_meth

    @lib_const_meth.setter
    @enforce_string
    def lib_const_meth(self, lib_const_meth):
        """
        The setter for the library construction method.

        Args:
            lib_const_meth (str): Library constuction method.T

        Returns:
            None
        """
        self.logger.debug("In 'lib_const_meth' setter.")

        self._lib_const_meth = lib_const_meth

    @property
    def lib_layout(self):
        """
        str: Specification of the layout: fragment/paired, and if paired,
             then the nominal insert size and standard deviation.
        """
        self.logger.debug("In 'lib_layout' getter.")

        return self._lib_layout

    @lib_layout.setter
    @enforce_string
    def lib_layout(self, lib_layout):
        """
        The setter for the lib layout: fragment/paired, and if paired, then
        the nominal insert size and standard deviation..

        Args:
            lib_layout (str): The new lib layout.

        Returns:
            None
        """
        self.logger.debug("In 'lib_layout' setter.")

        self._lib_layout = lib_layout

    @property
    def lib_screen(self):
        """
        str: Specific enrichment or screening methods applied before and/or
        after creating clone libraries.
        """
        self.logger.debug("In 'lib_screen' getter.")

        return self._lib_screen

    @lib_screen.setter
    @enforce_string
    def lib_screen(self, lib_screen):
        """
        The setter for the screening methods applied before and/or after
        creating clone libraries.

        Args:
            lib_screen (str): The new lib_screen.

        Returns:
            None
        """
        self.logger.debug("In 'lib_screen' setter.")

        self._lib_screen = lib_screen

    @property
    def lib_selection(self):
        """
        str: A controlled vocabulary of terms describing selection or reduction
             method used in library construction. Terms used by TCGA include
             (random, hybrid selection)
        """
        self.logger.debug("In 'lib_selection' getter.")

        return self._lib_selection

    @lib_selection.setter
    @enforce_string
    def lib_selection(self, lib_selection):
        """
        The setter for the lib selection.

        Args:
            lib_selection (str): The new lib selection.

        Returns:
            None
        """
        self.logger.debug("In 'lib_selection' setter.")

        self._lib_selection = lib_selection

    @property
    def lib_size(self):
        """
        str: total number of clones in the library prepared for the project.
        """
        self.logger.debug("In 'lib_size' getter.")

        return self._lib_size

    @lib_size.setter
    @enforce_int
    def lib_size(self, lib_size):
        """
        Set the total number of clones in the library prepared for the project.

        Args:
            lib_size (int): the total number of clones.

        Returns:
            None
        """
        self.logger.debug("In 'lib_size' setter.")

        self._lib_size = lib_size

    @property
    def lib_vector(self):
        """
        str: Vector type used in construction of libraries.
        """
        self.logger.debug("In 'lib_vector' getter.")

        return self._lib_vector

    @lib_vector.setter
    @enforce_string
    def lib_vector(self, lib_vector):
        """
        The setter for the cloning vector type used in construction of
        libraries.

        Args:
            lib_vector (str): Library construction method.

        Returns:
            None
        """
        self.logger.debug("In 'lib_vector' setter.")

        self._lib_vector = lib_vector

    @property
    def mims(self):
        """
        mims: Genomic Standards Consortium MIMS fields.
        """
        self.logger.debug("In 'mims' getter.")

        return self._mims

    @mims.setter
    @enforce_dict
    def mims(self, mims):
        """
        The setter for the MIMS data. The provided dictionary must validate
        based on the MIMS class.

        Args:
            mims (dict): A MIMS dictionary.

        Returns:
            None
        """
        self.logger.debug("In 'mims' setter.")

        valid_dictionary = MIMS.check_dict(mims)

        # Validate the incoming MIMS data
        if valid_dictionary:
            self.logger.debug("MIMS data seems correct.")
            self._mims = mims
        else:
            raise MimsException("Invalid MIMS data detected.")

    @property
    def ncbi_taxon_id(self):
        """
        str: NCBI taxon id.
        """
        self.logger.debug("In 'ncbi_taxon_id' getter.")

        return self._ncbi_taxon_id

    @ncbi_taxon_id.setter
    @enforce_string
    def ncbi_taxon_id(self, ncbi_taxon_id):
        """
        The setter for the NCBI Taxon ID.

        Args:
            ncbi_taxon_id (str): The new NCBI Taxon ID.

        Returns:
            None
        """
        self.logger.debug("In 'ncbi_taxon_id' setter.")

        self._ncbi_taxon_id = ncbi_taxon_id

    @property
    def nucl_acid_amp(self):
        """
        str: Nucleic acid amplification.
        """
        self.logger.debug("In 'nucl_acid_amp' getter.")

        return self._nucl_acid_amp

    @nucl_acid_amp.setter
    @enforce_string
    def nucl_acid_amp(self, nucl_acid_amp):
        """
        The setter for the nucleic acid amplification.

        Args:
            nucl_acid_amp (str): The nucleic acid amplification.

        Returns:
            None
        """
        self.logger.debug("In 'nucl_acid_amp' setter.")

        self._nucl_acid_amp = nucl_acid_amp

    @property
    def nucl_acid_ext(self):
        """
        str: Nucleic acid extraction.
        """
        self.logger.debug("In 'nucl_acid_ext' getter.")

        return self._nucl_acid_ext

    @nucl_acid_ext.setter
    @enforce_string
    def nucl_acid_ext(self, nucl_acid_ext):
        """
        The setter for the nucleic acid extraction.

        Args:
            nucl_acid_ext (str): The nucleic acid extraction.

        Returns:
            None
        """
        self.logger.debug("In 'nucl_acid_ext' setter.")

        self._nucl_acid_ext = nucl_acid_ext

    @property
    def prep_id(self):
        """
        str: nucleic acid prep ID.
        """
        self.logger.debug("In 'prep_id' getter.")

        return self._prep_id

    @prep_id.setter
    @enforce_string
    def prep_id(self, prep_id):
        """
        The setter for the nucleic acid prep ID.

        Args:
            prep_id (str): The new nucleic acid prep ID.

        Returns:
            None
        """
        self.logger.debug("In 'prep_id' setter.")

        self._prep_id = prep_id

    @property
    def rindex(self):
        """
        str: Reverse strand molecular barcode, called Multiplex Identifier (MID),
        that is used to specifically tag unique samples in a sequencing run.
        Sequence should be reported in uppercase letters.
        },
        """
        self.logger.debug("In 'rindex' getter.")

        return self._rindex

    @rindex.setter
    @enforce_string
    def rindex(self, rindex):
        """
        The setter for the reverse strand molecular barcode, called Multiplex
        Identifier (MID).

        Args:
            rindex (str): The string to set for rindex.

        Returns:
            None
        """
        self.logger.debug("In 'rindex' setter.")

        self._rindex = rindex

    @property
    def sequencing_center(self):
        """
        str: The center responsible for generating the prep.
        """
        self.logger.debug("In 'sequencing_center' getter.")

        return self._sequencing_center

    @sequencing_center.setter
    @enforce_string
    def sequencing_center(self, sequencing_center):
        """
        The setter for the sequencing center.

        Args:
            sequencing_center (str): The new sequencing center.

        Returns:
            None
        """
        self.logger.debug("In 'sequencing_center' setter.")

        self._sequencing_center = sequencing_center

    @property
    def sequencing_contact(self):
        """
        str: Name and email of the primary contact at the sequencing center.
        """
        self.logger.debug("In 'sequencing_contact' getter.")

        return self._sequencing_contact

    @sequencing_contact.setter
    @enforce_string
    def sequencing_contact(self, sequencing_contact):
        """
        The setter for the sequencing contact. Name and email of the primary
        contact at the center..

        Args:
            sequencing_contact (str): The new sequencing contact.

        Returns:
            None
        """
        self.logger.debug("In 'sequencing_contact' setter.")

        self._sequencing_contact = sequencing_contact

    @property
    def srs_id(self):
        """
        str: NCBI Sequence Read Archive sample ID of the form SRS012345.
        """
        self.logger.debug("In 'srs_id' getter.")

        return self._srs_id

    @srs_id.setter
    @enforce_string
    def srs_id(self, srs_id):
        """
        The setter for the SRS ID. The ID must be a string, and greater than
        3 characters long.

        Args:
            srs_id (str): The new SRS ID.

        Returns:
            None
        """
        self.logger.debug("In 'srs_id' setter.")

        if len(srs_id) < 3:
            raise Exception("SRS ID is too short, must be more than 3 characters.")

        self._srs_id = srs_id

    @property
    def samp_mat_process(self):
        """
        str: Any processing applied to the sample during or after retrieving
        the sample from environment. This field accepts OBI, for a browser of
        OBI (v 2013-10-25) terms please see
        http://purl.bioontology.org/ontology/OBI
        """
        self.logger.debug("In 'samp_mat_process' getter.")

        return self._samp_mat_process

    @samp_mat_process.setter
    @enforce_int
    def samp_mat_process(self, samp_mat_process):
        """
        The setter for 'samp_mat_process'.

        Args:
            samp_mat_process (str): The samp_mat_process value.

        Returns:
            None
        """
        self.logger.debug("In 'samp_mat_process' setter.")

        self._samp_mat_process = samp_mat_process

    @property
    def storage_duration(self):
        """
        int: Duration for which sample was stored in days.
        """
        self.logger.debug("In 'storage_duration' getter.")

        return self._storage_duration

    @storage_duration.setter
    @enforce_int
    def storage_duration(self, storage_duration):
        """
        The setter for the HostSeqPrep storage duration. The duration must
        be an integer, and greater than 0.

        Args:
            storage_duration (int): The new storage duration.

        Returns:
            None
        """
        self.logger.debug("In 'storage_duration' setter.")

        if storage_duration < 0:
            raise ValueError("Invalid storage_duration. Must be non-negative.")

        self._storage_duration = storage_duration

    @staticmethod
    def required_fields():
        """
        A static method. The required fields for the class.

        Args:
            None
        Returns:
            Tuple of strings of required properties.
        """
        module_logger.debug("In required_fields.")
        return ("comment", "lib_layout", "lib_selection",
                "ncbi_taxon_id", "prep_id", "sequencing_center",
                "sequencing_contact", "storage_duration", "tags")

    def _get_raw_doc(self):
        """
        Generates the raw JSON document for the current object. All required
        fields are filled into the JSON document, regardless they are set or
        not. Any remaining fields are included only if they are set. This
        allows the user to visualize the JSON to ensure fields are set
        appropriately before saving into the database.

        Args:
            None

        Returns:
            A dictionary representation of the JSON document.
        """
        self.logger.debug("In _get_raw_doc.")

        doc = {
            'acl': {
                'read': ['all'],
                'write': [HostSeqPrep.namespace]
            },
            'linkage': self._links,
            'ns': HostSeqPrep.namespace,
            'node_type': 'host_seq_prep',
            'meta': {
                'comment': self._comment,
                'lib_layout': self._lib_layout,
                'lib_selection': self._lib_selection,
                'ncbi_taxon_id': self._ncbi_taxon_id,
                'prep_id': self._prep_id,
                'sequencing_center': self._sequencing_center,
                'sequencing_contact': self._sequencing_contact,
                'storage_duration': self._storage_duration,
                'subtype': "host",
                'tags': self._tags
            }
        }

        if self._id is not None:
            self.logger.debug("%s object has the OSDF id set.", __name__)
            doc['id'] = self._id

        if self._version is not None:
            self.logger.debug("%s object has the OSDF version set.", __name__)
            doc['ver'] = self._version

        if self._adapters is not None:
            self.logger.debug("%s object has adapters set.", __name__)
            doc['meta']['adapters'] = self._adapters

        if self._experimental_factor is not None:
            self.logger.debug("%s object has experimental_factor set.", __name__)
            doc['meta']['experimental_factor'] = self._experimental_factor

        if self._findex is not None:
            self.logger.debug("%s object has findex set.", __name__)
            doc['meta']['findex'] = self._findex

        if self._frag_size is not None:
            self.logger.debug("%s object has frag_size set.", __name__)
            doc['meta']['frag_size'] = self._frag_size

        if self._lib_const_meth is not None:
            self.logger.debug("%s object has lib_const_meth set.", __name__)
            doc['meta']['lib_const_meth'] = self._lib_const_meth

        if self._lib_screen is not None:
            self.logger.debug("%s object has lib_screen set.", __name__)
            doc['meta']['lib_screen'] = self._lib_screen

        if self._lib_size is not None:
            self.logger.debug("%s object has lib_size set.", __name__)
            doc['meta']['lib_size'] = self._lib_size

        if self._lib_vector is not None:
            self.logger.debug("%s object has lib_vector set.", __name__)
            doc['meta']['lib_vector'] = self._lib_vector

        if self._mims is not None:
            self.logger.debug("%s object has mims set.", __name__)
            doc['meta']['mims'] = self._mims

        if self._nucl_acid_amp is not None:
            self.logger.debug("%s object has nucl_acid_amp set.", __name__)
            doc['meta']['nucl_acid_amp'] = self._nucl_acid_amp

        if self._nucl_acid_ext is not None:
            self.logger.debug("%s object has nucl_acid_ext set.", __name__)
            doc['meta']['nucl_acid_ext'] = self._nucl_acid_ext

        if self._rindex is not None:
            self.logger.debug("%s object has rindex set.", __name__)
            doc['meta']['rindex'] = self._rindex

        if self._srs_id is not None:
            self.logger.debug("%s object has srs_id set.", __name__)
            doc['meta']['srs_id'] = self._srs_id

        return doc

    @staticmethod
    def load_host_seq_prep(prep_data):
        """
        Takes the provided JSON string and converts it to a HostSeqPrep object.

        Args:
            prep_data (str): The JSON string to convert

        Returns:
            Returns a HostSeqPrep instance.
        """
        module_logger.info("Creating a template %s.", __name__)
        prep = HostSeqPrep()

        module_logger.debug("Filling in %s details.", __name__)

        # The attributes commmon to all iHMP nodes
        prep._set_id(prep_data['id'])
        prep.version = prep_data['ver']
        prep.links = prep_data['linkage']
        prep.tags = prep_data['meta']['tags']

        # The attributes that are particular to HostSeqPrep documents
        prep.comment = prep_data['meta']['comment']
        prep.lib_layout = prep_data['meta']['lib_layout']
        prep.lib_selection = prep_data['meta']['lib_selection']
        prep.ncbi_taxon_id = prep_data['meta']['ncbi_taxon_id']
        prep.prep_id = prep_data['meta']['prep_id']
        prep.sequencing_center = prep_data['meta']['sequencing_center']
        prep.sequencing_contact = prep_data['meta']['sequencing_contact']
        prep.storage_duration = prep_data['meta']['storage_duration']

        if 'adapters' in prep_data['meta']:
            module_logger.info("%s data has 'adapters' present.", __name__)
            prep.adapters = prep_data['meta']['adapters']

        if 'experimental_factor' in prep_data['meta']:
            module_logger.info("%s data has 'experimental_factor' present.", __name__)
            prep.experimental_factor = prep_data['meta']['experimental_factor']

        if 'findex' in prep_data['meta']:
            module_logger.info("%s data has 'findex' present.", __name__)
            prep.findex = prep_data['meta']['findex']

        if 'frag_size' in prep_data['meta']:
            module_logger.info("%s data has 'frag_size' present.", __name__)
            prep.frag_size = prep_data['meta']['frag_size']

        if 'lib_const_meth' in prep_data['meta']:
            module_logger.info("%s data has 'lib_const_meth' present.", __name__)
            prep.lib_const_meth = prep_data['meta']['lib_const_meth']

        if 'lib_screen' in prep_data['meta']:
            module_logger.info("%s data has 'lib_screen' present.", __name__)
            prep.lib_screen = prep_data['meta']['lib_screen']

        if 'lib_size' in prep_data['meta']:
            module_logger.info("%s data has 'lib_size' present.", __name__)
            prep.lib_size = prep_data['meta']['lib_size']

        if 'lib_vector' in prep_data['meta']:
            module_logger.info("%s data has 'lib_vector' present.", __name__)
            prep.lib_vector = prep_data['meta']['lib_vector']

        if 'mims' in prep_data['meta']:
            module_logger.info("%s data has 'mims' present.", __name__)
            prep.mims = prep_data['meta']['mims']

        if 'nucl_acid_amp' in prep_data['meta']:
            module_logger.info("%s data has 'nucl_acid_amp' present.", __name__)
            prep.nucl_acid_amp = prep_data['meta']['nucl_acid_amp']

        if 'nucl_acid_ext' in prep_data['meta']:
            module_logger.info("%s data has 'nucl_acid_amp' present.", __name__)
            prep.nucl_acid_ext = prep_data['meta']['nucl_acid_ext']

        if 'rindex' in prep_data['meta']:
            module_logger.info("%s data has 'rindex' present.", __name__)
            prep.rindex = prep_data['meta']['rindex']

        if 'samp_mat_process' in prep_data['meta']:
            module_logger.info("%s data has 'samp_mat_process' present.", __name__)
            prep.samp_mat_process = prep_data['meta']['samp_mat_process']

        if 'srs_id' in prep_data['meta']:
            module_logger.info("%s data has 'srs_id' present.", __name__)
            prep.srs_id = prep_data['meta']['srs_id']

        module_logger.debug("Returning loaded %s", __name__)

        return prep

    @staticmethod
    def load(prep_id):
        """
        Loads the data for the specified node ID from OSDF to this object.  If
        the provided ID does not exist, then an error message is provided
        stating the project does not exist.

        Args:
            prep_id (str): The OSDF ID for the document to load.

        Returns:
            A HostSeqPrep object with all the available OSDF data loaded into it.
        """
        module_logger.debug("In load. Specified ID: %s", prep_id)

        session = iHMPSession.get_session()
        module_logger.info("Got iHMP session.")

        module_logger.info("Retrieving data for %s.", __name__)
        prep_data = session.get_osdf().get_node(prep_id)

        prep = HostSeqPrep.load_host_seq_prep(prep_data)

        return prep

    def save(self):
        """
        Saves the data in the current instance. The JSON form of the current
        data for the instance is validated in the save function. If the data is
        not valid, then the data will not be saved. If the instance was saved
        previously, then the node ID is assigned the alpha numeric found in the
        OSDF instance. If not saved previously, then the node ID is 'None', and
        upon success, will be assigned to the alphanumeric ID found in OSDF.
        Also, the version is updated as the data is saved in OSDF.

        Args:
            None

        Returns;
            True if successful, False otherwise.

        """
        self.logger.debug("In save.")

        if not self.is_valid():
            self.logger.error("Cannot save, data is invalid")
            return False

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        success = False

        if self.id is None:
            # The document has not yet been saved
            prep_data = self._get_raw_doc()
            self.logger.info("Got the raw JSON document.")

            try:
                self.logger.info("Attempting to save a new node.")
                node_id = session.get_osdf().insert_node(prep_data)
                self.logger.info("Save for HostSeqPrep %s successful.", node_id)
                self.logger.info("Setting ID for HostSeqPrep %s.", node_id)

                self._set_id(node_id)
                self._version = 1
                success = True
            except Exception as insert_exception:
                self.logger.error("An error occurred while inserting " + \
                                  "%s %s. Reason: %s", __name__, self._id,
                                  insert_exception
                                 )
        else:
            prep_data = self._get_raw_doc()

            try:
                self.logger.info("Attempting to update %s with ID: %s.", __name__, self._id)
                session.get_osdf().edit_node(prep_data)
                self.logger.info("Update for %s %s successful.", __name__, self._id)
                success = True
            except Exception as edit_exception:
                self.logger.error("An error occurred while updating %s " + \
                                  " %s. Reason: %s", __name__, self._id,
                                  edit_exception
                                 )

        return success

    @staticmethod
    def search(query="\"host_seq_prep\"[node_type]"):
        """
        Searches the OSDF database through all HostSeqPrep nodes. Any criteria
        the user wishes to add is provided by the user in the query language
        specifications provided in the OSDF documentation. A general format
        is (including the quotes and brackets):

        "search criteria"[field to search]

        If there are any results, they are returned as HostSeqPrep instances,
        otherwise an empty list will be returned.

        Args:
            query (str): The query for the OSDF framework. Defaults to the
                         HostSeqPrep node type.

        Returns:
            Returns an array of HostSeqPrep objects. It returns an empty list if
            there are no results.
        """
        module_logger.debug("In search.")

        session = iHMPSession.get_session()
        module_logger.info("Got iHMP session.")

        if query != '"host_seq_prep"[node_type]':
            query = '({}) && "host_seq_prep"[node_type]'.format(query)

        module_logger.debug("Submitting OQL query: %s", query)

        prep_data = session.get_osdf().oql_query(HostSeqPrep.namespace, query)

        all_results = prep_data['results']

        result_list = list()

        if len(all_results) > 0:
            for result in all_results:
                prep_result = HostSeqPrep.load_host_seq_prep(result)
                result_list.append(prep_result)

        return result_list

    def _derived_docs(self):
        self.logger.debug("In _derived_docs().")

        linkage_query = '"{}"[linkage.sequenced_from]'.format(self.id)
        query = iHMPSession.get_session().get_osdf().oql_query

        for page_no in count(1):
            res = query(HostSeqPrep.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield doc
            res_count -= len(res['results'])

            if res_count < 1:
                break

    def derivations(self):
        """
        Return an iterator of all the derived nodes from this prep.
        """
        self.logger.debug("In derivations().")

        from cutlass.HostWgsRawSeqSet import HostWgsRawSeqSet
        from cutlass.HostTranscriptomicsRawSeqSet import HostTranscriptomicsRawSeqSet

        for doc in self._derived_docs():
            if doc['node_type'] == "host_transcriptomics_raw_seq_set":
                yield HostTranscriptomicsRawSeqSet.load_host_transcriptomics_raw_seq_set(doc)
            elif doc['node_type'] == "host_wgs_raw_seq_set":
                yield HostWgsRawSeqSet.load_hostWgsRawSeqSet(doc) class MicrobiomeAssayPrep(Base):
    """
    The class encapsulates iHMP microbiome assay prep data.  It contains all
    the fields required to save a such an object in OSDF.

    Attributes:
        namespace (str): The namespace this class will use in OSDF.
    """
    namespace = "ihmp"

    def __init__(self, *args, **kwargs):
        """
        Constructor for the MicrobiomeAssayPrep class. This initializes the
        fields specific to the class, and inherits from the Base class.

        Args:
            None
        """
        self.logger = logging.getLogger(self.__module__ + '.' + self.__class__.__name__)

        self.logger.addHandler(logging.NullHandler())

        self._id = None
        self._version = None
        self._links = {}
        self._tags = []

        # Required properties
        self._comment = None
        self._pride_id = None
        self._sample_name = None
        self._title = None
        self._center = None
        self._contact = None
        self._prep_id = None
        self._storage_duration = None
        self._experiment_type = None
        self._study = None

        # Optional properties
        self._short_label = None
        self._url = None
        self._species = None
        self._cell_type = None
        self._tissue = None
        self._reference = None
        self._protocol_name = None
        self._protocol_steps = None
        self._exp_description = None
        self._sample_description = None

        super(MicrobiomeAssayPrep, self).__init__(*args, **kwargs)

    @property
    def comment(self):
        """
        str: A descriptive comment for the object.
        """
        self.logger.debug("In 'comment' getter.")

        return self._comment

    @comment.setter
    @enforce_string
    def comment(self, comment):
        """
        The setter for a descriptive comment for the object.

        Args:
            comment (str): The comment text.

        Returns:
            None
        """
        self.logger.debug("In 'comment' setter.")

        self._comment = comment

    @property
    def pride_id(self):
        """
        str: PRIDE identifier corresponding to study.
        """
        self.logger.debug("In 'pride_id' getter.")

        return self._pride_id

    @pride_id.setter
    @enforce_string
    def pride_id(self, pride_id):
        """
        The setter for the PRIDE identifier corresponding to study.

        Args:
            pride_id (str): The PRIDE identifier

        Returns:
            None
        """
        self.logger.debug("In 'pride_id' setter.")

        self._pride_id = pride_id

    @property
    def sample_name(self):
        """
        str: The short label that is referable to the sample used to
        generate the dataset.
        """
        self.logger.debug("In 'sample_name' getter.")

        return self._sample_name

    @sample_name.setter
    @enforce_string
    def sample_name(self, sample_name):
        """
        The setter for the short label that is referable to the sample
        used to generate the dataset.

        Args:
            sample_name (str): Short label for the sample.

        Returns:
            None
        """
        self.logger.debug("In 'sample_name' setter.")

        self._sample_name = sample_name

    @property
    def title(self):
        """
        str: The description of the particular experiment.
        """
        self.logger.debug("In 'title' getter.")

        return self._title

    @title.setter
    def title(self, title):
        """
        The setter for the description of the particular experiment.

        Args:
            title (str): Experiment title

        Returns:
            None
        """
        self.logger.debug("In title setter.")
        if type(title) is not str:
            raise ValueError("Invalid type for title.")

        self._title = title

    @property
    def short_label(self):
        """
        str: The short label/nomenclature used to group/organize experiments.
        """
        self.logger.debug("In 'short_label' getter.")

        return self._short_label

    @short_label.setter
    @enforce_string
    def short_label(self, short_label):
        """
        Set the short label/nomenclature used to group/organize experiments.

        Args:
            short_label (str): Short label used to group experiments

        Returns:
            None
        """
        self.logger.debug("In 'short_label' setter.")

        self._short_label = short_label

    @property
    def center(self):
        """
        str: The center responsible for generating the microbiome assay Prep.
        """
        self.logger.debug("In 'center' getter.")

        return self._center

    @center.setter
    @enforce_string
    def center(self, center):
        """
        Set the center responsible for generating the microbiome assay prep.

        Args:
            center (str): The center responsible for generating the microbiome assay prep.

        Returns:
            None
        """
        self.logger.debug("In 'center' setter.")

        self._center = center

    @property
    def contact(self):
        """
        str: Get the name and email of the primary contact at the center.
        """
        self.logger.debug("In 'contact' getter.")

        return self._contact

    @contact.setter
    @enforce_string
    def contact(self, contact):
        """
        Set the name and email of the primary contact at the center.

        Args:
            contact (str): Name and email of the primary contact at the center.

        Returns:
            None
        """
        self.logger.debug("In 'contact' setter.")

        self._contact = contact

    @property
    def prep_id(self):
        """
        str: Get the internal assay prep ID.
        """
        self.logger.debug("In 'prep_id' getter.")

        return self._prep_id

    @prep_id.setter
    @enforce_string
    def prep_id(self, prep_id):
        """
        Set the internal assay prep ID.

        Args:
            prep_id (str): Internal assay prep ID.

        Returns:
            None
        """
        self.logger.debug("In 'prep_id' setter.")

        self._prep_id = prep_id

    @property
    def storage_duration(self):
        """
        int: Get the MIGS/MIMS storage duration in days.
        """
        self.logger.debug("In storage_duration getter.")

        return self._storage_duration

    @storage_duration.setter
    @enforce_int
    def storage_duration(self, storage_duration):
        """
        Set the MIGS/MIMS storage duration in days.

        Args:
            storage_duration (int): Storage duration in days.

        Returns:
            None
        """
        self.logger.debug("In 'storage_duration' setter.")

        self._storage_duration = storage_duration

    @property
    def experiment_type(self):
        """
        str: Get the PRIDE experiment type.
        """
        self.logger.debug("In 'experiment_type' getter.")

        return self._experiment_type

    @experiment_type.setter
    @enforce_string
    def experiment_type(self, experiment_type):
        """
        Set the PRIDE experiment type.

        Args:
            experiment_type (str): Experiment type, as defined by PRIDE.

        Returns:
            None
        """
        self.logger.debug("In 'experiment_type' setter.")

        self._experiment_type = experiment_type

    @property
    def species(self):
        """
        str: Controlled vocabulary term to describe a single species. NEWT CV
        terms are allowed.
        """
        self.logger.debug("In 'species' getter.")

        return self._species

    @species.setter
    @enforce_string
    def species(self, species):
        """
        Controlled vocabulary term to describe a single species. NEWT CV
        terms are allowed.

        Args:
            species (str): Term to describe a single species.

        Returns:
            None
        """
        self.logger.debug("In 'species' setter.")

        self._species = species

    @property
    def cell_type(self):
        """
        str: Controlled vocabulary term to describe a single cell type. Cell
        type ontology CV terms are allowed.
        """
        self.logger.debug("In 'cell_type' getter.")

        return self._cell_type

    @cell_type.setter
    @enforce_string
    def cell_type(self, cell_type):
        """
        Controlled vocabulary term to describe a single cell type. Cell
        type ontology CV terms are allowed.

        Args:
            cell_type (str): Term to describe the cell type.

        Returns:
            None
        """
        self.logger.debug("In 'cell_type' setter.")

        self._cell_type = cell_type

    @property
    def tissue(self):
        """
        str: Controlled vocabulary term to describe a single tissue. BRENDA
        Tissue CV terms are allowed.
        """
        self.logger.debug("In 'tissue' getter.")

        return self._tissue

    @tissue.setter
    @enforce_string
    def tissue(self, tissue):
        """
        Controlled vocabulary term to describe a single tissue. BRENDA Tissue
        CV terms are allowed.

        Args:
            tissue (str): Term to describe the tissue.

        Returns:
            None
        """
        self.logger.debug("In 'tissue' setter.")

        self._tissue = tissue

    @property
    def reference(self):
        """
        str: Link to literature citation for which this experiment provides
        supporting evidence.
        """
        self.logger.debug("In 'reference' getter.")

        return self._reference

    @reference.setter
    @enforce_string
    def reference(self, reference):
        """
        Set the literature citation for which this experiment provides
        supporting evidence.

        Args:
            reference (str): Supporting evidence link.

        Returns:
            None
        """
        self.logger.debug("In 'reference' setter.")

        self._reference = reference

    @property
    def protocol_name(self):
        """
        str: The protocol title with versioning.
        """
        self.logger.debug("In 'protocol_name' getter.")

        return self._protocol_name

    @protocol_name.setter
    @enforce_string
    def protocol_name(self, protocol_name):
        """
        Set the protocol title with versioning.

        Args:
            protocol_name (str): Protocol title with versioning, ideally,
            pointing to a URL.

        Returns:
            None
        """
        self.logger.debug("In 'protocol_name' setter.")

        self._protocol_name = protocol_name

    @property
    def protocol_steps(self):
        """
        str: Description of the sample processing steps.
        """
        self.logger.debug("In 'protocol_steps' getter.")

        return self._protocol_steps

    @protocol_steps.setter
    @enforce_string
    def protocol_steps(self, protocol_steps):
        """
        Set the description of the sample processing steps.

        Args:
            protocol_name (str): Protocol title with versioning, ideally,
            pointing to a URL.

        Returns:
            None
        """
        self.logger.debug("In 'protocol_steps' setter.")

        self._protocol_steps = protocol_steps

    @property
    def exp_description(self):
        """
        str: Description of the goals and objectives of this study.
        """
        self.logger.debug("In 'exp_description' getter.")

        return self._exp_description

    @exp_description.setter
    @enforce_string
    def exp_description(self, exp_description):
        """
        Set the description of the goals and objectives of this study,
        summary of the abstract, optimally 2-3 sentences.

        Args:
            exp_description (str): Description of the goals/objectives
            of the study.

        Returns:
            None
        """
        self.logger.debug("In 'exp_description' setter.")

        self._exp_description = exp_description

    @property
    def sample_description(self):
        """
        str: Expansible description of the sample used to generate the
        dataset.
        """
        self.logger.debug("In 'sample_description' getter.")

        return self._sample_description

    @sample_description.setter
    @enforce_string
    def sample_description(self, sample_description):
        """
        Set the expansible description of the sample used to generate the
        dataset

        Args:
            sample_description (str): Expansible description of the sample
            used to generate the dataset

        Returns:
            None
        """
        self.logger.debug("In 'sample_description' setter.")

        self._sample_description = sample_description

    @property
    def study(self):
        """
        str: One of the 3 studies that are part of the iHMP.
        """
        self.logger.debug("In 'study' getter.")

        return self._study

    @study.setter
    @enforce_string
    def study(self, study):
        """
        One of the 3 studies that are part of the iHMP.

        Args:
            study (str): One of the 3 studies that are part of the iHMP.

        Returns:
            None
        """
        self.logger.debug("In 'study' setter.")

        self._study = study

    def validate(self):
        """
        Validates the current object's data/JSON against the current
        schema in the OSDF instance for that specific object. All required
        fields for that specific object must be present.

        Args:
            None

        Returns:
            A list of strings, where each string is the error that the
            validation raised during OSDF validation
        """
        self.logger.debug("In validate.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        (valid, error_message) = session.get_osdf().validate_node(document)

        problems = []
        if not valid:
            self.logger.info("Validation did not succeed.")
            problems.append(error_message)

        if 'prepared_from' not in self._links.keys():
            problems.append("Must have a 'prepared_from' link to a sample.")

        self.logger.debug("Number of validation problems: %s.", len(problems))
        return problems

    def is_valid(self):
        """
        Validates the current object's data/JSON against the current schema
        in the OSDF instance for the specific object. However, unlike
        validates(), this method does not provide exact error messages,
        it states if the validation was successful or not.

        Args:
            None

        Returns:
            True if the data validates, False if the current state of
            fields in the instance do not validate with the OSDF instance
        """
        self.logger.debug("In is_valid.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        (valid, _error_message) = session.get_osdf().validate_node(document)

        if 'prepared_from' not in self._links.keys():
            valid = False

        self.logger.debug("Valid? %s", str(valid))

        return valid

    def _get_raw_doc(self):
        """
        Generates the raw JSON document for the current object. All required fields are
        filled into the JSON document, regardless they are set or not. Any remaining
        fields are included only if they are set. This allows the user to visualize
        the JSON to ensure fields are set appropriately before saving into the
        database.

        Args:
            None

        Returns:
            An object representation of the JSON document.
        """
        self.logger.debug("In _get_raw_doc.")

        prep_doc = {
            'acl': {
                'read': ['all'],
                'write': [MicrobiomeAssayPrep.namespace]
            },
            'linkage': self._links,
            'ns': MicrobiomeAssayPrep.namespace,
            'node_type': 'microb_assay_prep',
            'meta': {
                'center': self._center,
                'comment': self._comment,
                'contact': self._contact,
                'experiment_type': self._experiment_type,
                'prep_id': self._prep_id,
                'pride_id': self._pride_id,
                'sample_name': self._sample_name,
                'storage_duration': self._storage_duration,
                'study': self._study,
                'subtype': self._study,
                'tags': self._tags,
                'title': self._title
            }
        }

        if self._id is not None:
            self.logger.debug("%s object has the OSDF id set.", __name__)
            prep_doc['id'] = self._id

        if self._version is not None:
            self.logger.debug("%s object has the OSDF version set.", __name__)
            prep_doc['ver'] = self._version

        # Handle optional properties
        if self._short_label is not None:
            self.logger.debug("%s object has the 'short_label' property set.", __name__)
            prep_doc['meta']['short_label'] = self._short_label

        if self._url is not None:
            self.logger.debug("%s object has the 'url' property set.", __name__)
            prep_doc['meta']['url'] = self._url

        if self._species is not None:
            self.logger.debug("%s object has the 'species' property set.", __name__)
            prep_doc['meta']['species'] = self._species

        if self._cell_type is not None:
            self.logger.debug("%s object has the 'cell_type' property set.", __name__)
            prep_doc['meta']['cell_type'] = self._cell_type

        if self._tissue is not None:
            self.logger.debug("%s object has the 'tissue' property set.", __name__)
            prep_doc['meta']['tissue'] = self._tissue

        if self._reference is not None:
            self.logger.debug("%s object has the 'reference' property set.", __name__)
            prep_doc['meta']['reference'] = self._reference

        if self._protocol_name is not None:
            self.logger.debug("%s object has the 'protocol_name' property set.", __name__)
            prep_doc['meta']['protocol_name'] = self._protocol_name

        if self._protocol_steps is not None:
            self.logger.debug("%s object has the 'protocol_steps' property set.", __name__)
            prep_doc['meta']['protocol_steps'] = self._protocol_steps

        if self._exp_description is not None:
            self.logger.debug("%s object has the 'exp_description' property set.", __name__)
            prep_doc['meta']['exp_description'] = self._exp_description

        if self._sample_description is not None:
            self.logger.debug("%s object has the 'sample_description' property set.", __name__)
            prep_doc['meta']['sample_description'] = self._sample_description

        return prep_doc

    @staticmethod
    def required_fields():
        """
        A static method. The required fields for the class.

        Args:
            None
        Returns:
            Tuple of strings of required properties.
        """
        module_logger.debug("In required fields.")
        return ("comment", "pride_id", "sample_name", "title",
                "center", "contact", "prep_id", "storage_duration",
                "experiment_type", "study", "tags")

    def delete(self):
        """
        Deletes the current object (self) from OSDF. If the object has not been
        previously saved (node ID is not set), then an error message will be
        logged stating the object was not deleted. If the ID is set, and exists
        in the OSDF instance, then the object will be deleted from the OSDF
        instance, and this object must be re-saved in order to use it again.

        Args:
            None

        Returns:
            True upon successful deletion, False otherwise.
        """
        self.logger.debug("In delete.")

        if self._id is None:
            self.logger.warn("Attempt to delete a %s with no ID.", __name__)
            raise Exception("{} does not have an ID.".format(__name__))

        prep_id = self._id

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        # Assume failure
        success = False

        try:
            self.logger.info("Deleting %s with ID %s.", __name__, prep_id)
            session.get_osdf().delete_node(prep_id)
            success = True
        except Exception as delete_exception:
            self.logger.exception(delete_exception)
            self.logger.error("An error occurred when deleting %s.", self)

        return success

    @staticmethod
    def search(query="\"microb_assay_prep\"[node_type]"):
        """
        Searches OSDF for MicrobiomeAssayPrep nodes. Any criteria the user
        wishes to add is provided by the user in the query language
        specifications provided in the OSDF documentation. A general format is
        (including the quotes and brackets):

        "search criteria"[field to search]

        If there are any results, they are returned as MicrobiomeAssayPrep instances,
        otherwise an empty list will be returned.

        Args:
            query (str): The query for the OSDF framework. Defaults to the
                         MicrobiomeAssayPrep node type.

        Returns:
            Returns an array of MicrobiomeAssayPrep objects. It returns an
            empty list if there are no results.
        """
        module_logger.debug("In search.")

        session = iHMPSession.get_session()
        module_logger.info("Got iHMP session.")

        if query != '"microb_assay_prep"[node_type]':
            query = '({}) && "microb_assay_prep"[node_type]'.format(query)

        module_logger.debug("Submitting OQL query: %s", query)

        prep_data = session.get_osdf().oql_query(
            MicrobiomeAssayPrep.namespace, query
        )

        all_results = prep_data['results']

        result_list = list()

        if len(all_results) > 0:
            for result in all_results:
                prep_result = MicrobiomeAssayPrep.load_microassayprep(result)
                result_list.append(prep_result)

        return result_list

    @staticmethod
    def load_microassayprep(prep_data):
        """
        Takes the provided JSON string and converts it to a
        MicrobiomeAssayPrep object

        Args:
            prep_data (str): The JSON string to convert

        Returns:
            Returns a MicrobiomeAssayPrep instance.
        """
        module_logger.info("Creating a template %s.", __name__)
        prep = MicrobiomeAssayPrep()

        module_logger.debug("Filling in %s details.", __name__)
        prep._set_id(prep_data['id'])
        prep.links = prep_data['linkage']
        prep.version = prep_data['ver']

        # Required fields
        prep.comment = prep_data['meta']['comment']
        prep.contact = prep_data['meta']['contact']
        prep.pride_id = prep_data['meta']['pride_id']
        prep.sample_name = prep_data['meta']['sample_name']
        prep.title = prep_data['meta']['title']
        prep.center = prep_data['meta']['center']
        prep.prep_id = prep_data['meta']['prep_id']
        prep.storage_duration = prep_data['meta']['storage_duration']
        prep.experiment_type = prep_data['meta']['experiment_type']
        prep.study = prep_data['meta']['study']
        prep.tags = prep_data['meta']['tags']

        # Optional fields
        if 'short_label' in prep_data['meta']:
            prep.short_label = prep_data['meta']['short_label']

        if 'url' in prep_data['meta']:
            prep._url = prep_data['meta']['url']

        if 'species' in prep_data['meta']:
            prep.species = prep_data['meta']['species']

        if 'cell_type' in prep_data['meta']:
            prep.cell_type = prep_data['meta']['cell_type']

        if 'tissue' in prep_data['meta']:
            prep.tissue = prep_data['meta']['tissue']

        if 'reference' in prep_data['meta']:
            prep.reference = prep_data['meta']['reference']

        if 'protocol_name' in prep_data['meta']:
            prep.protocol_name = prep_data['meta']['protocol_name']

        if 'protocol_steps' in prep_data['meta']:
            prep.protocol_steps = prep_data['meta']['protocol_steps']

        if 'exp_description' in prep_data['meta']:
            prep.exp_description = prep_data['meta']['exp_description']

        if 'sample_description' in prep_data['meta']:
            prep.sample_description = prep_data['meta']['sample_description']

        module_logger.debug("Returning loaded %s.", __name__)
        return prep

    @staticmethod
    def load(node_id):
        """
        Loads the data for the specified input ID from the OSDF instance to this object.
        If the provided ID does not exist, then an error message is provided stating the
        project does not exist.

        Args:
            node_id (str): The OSDF ID for the document to load.

        Returns:
            A MicrobiomeAssayPrep object with all the available OSDF data loaded into it.
        """
        module_logger.debug("In load. Specified ID: %s", node_id)

        session = iHMPSession.get_session()
        module_logger.info("Got iHMP session.")
        node_data = session.get_osdf().get_node(node_id)
        node = MicrobiomeAssayPrep.load_microassayprep(node_data)

        module_logger.debug("Returning loaded %s.", __name__)

        return node

    def save(self):
        """
        Saves the data in OSDF. The JSON form of the current data for the
        instance is validated in the save function. If the data is not valid,
        then the data will not be saved. If the instance was saved previously,
        then the node ID is assigned the alpha numeric found in the OSDF
        instance. If not saved previously, then the node ID is 'None', and upon
        a successful, will be assigned to the alpha numeric ID found in OSDF.
        Also, the version is updated as the data is saved in OSDF.

        Args:
            None

        Returns;
            True if successful, False otherwise.

        """
        self.logger.debug("In save.")

        # If node previously saved, use edit_node instead since ID
        # is given (an update in a way)
        # can also use get_node to check if the node already exists
        if not self.is_valid():
            self.logger.error("Cannot save, data is invalid.")
            return False

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        osdf = session.get_osdf()

        success = False

        if self._id is None:
            self.logger.info("About to insert a new %s OSDF node.", __name__)

            # Get the JSON form of the data and load it
            self.logger.debug("Converting %s to parsed JSON form.", __name__)
            data = json.loads(self.to_json())

            try:
                node_id = osdf.insert_node(data)

                self._set_id(node_id)
                self._version = 1
                success = True
            except Exception as save_exception:
                self.logger.exception(save_exception)
                self.logger.error("An error occurred when saving %s.", self)
        else:
            self.logger.info("%s already has an ID, so we do an update "
                             "(not an insert).", __name__)

            try:
                prep_data = self._get_raw_doc()
                self.logger.info("%s already has an ID, "
                                 "so we do an update (not an insert).", __name__)
                prep_id = self._id
                self.logger.debug("%s OSDF ID to update: %s.", __name__, prep_id)
                osdf.edit_node(prep_data)

                prep_data = osdf.get_node(prep_id)
                latest_version = prep_data['ver']

                self.logger.debug("The version of this %s is now: %s",
                                  __name__, str(latest_version))
                self._version = latest_version
                success = True
            except Exception as update_exception:
                self.logger.exception(update_exception)
                self.logger.error("An error occurred when updating %s.", self)

        return success

    def cytokines(self):
        """
        Returns an iterator of all Cytokines connected to this MicrobiomeAssayPrep.
        """
        self.logger.debug("In cytokines().")

        linkage_query = '"{}"[linkage.derived_from] && "cytokine"[node_type]'.format(self.id)

        query = iHMPSession.get_session().get_osdf().oql_query

        from cutlass.Cytokine import Cytokine

        for page_no in count(1):
            res = query(MicrobiomeAssayPrep.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield Cytokine.load_cytokine(doc)

            res_count -= len(res['results'])

            if res_count < 1:
                break

    def lipidomes(self):
        """
        Returns an iterator of all Lipidomes connected to this
        MicrobiomeAssayPrep.
        """
        self.logger.debug("In lipidomes().")

        linkage_query = '"{}"[linkage.derived_from] and "lipidome"[node_type]'.format(self.id)

        query = iHMPSession.get_session().get_osdf().oql_query

        from cutlass.Lipidome import Lipidome

        for page_no in count(1):
            res = query(MicrobiomeAssayPrep.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield Lipidome.load_lipidome(doc)

            res_count -= len(res['results'])

            if res_count < 1:
                break

    def metabolomes(self):
        """
        Returns an iterator of all Metabolomes connected to this
        MicrobiomeAssayPrep.
        """
        self.logger.debug("In metabolomes().")

        linkage_query = '"{}"[linkage.derived_from] && "metabolome"[node_type]'.format(self.id)

        query = iHMPSession.get_session().get_osdf().oql_query

        from cutlass.Metabolome import Metabolome

        for page_no in count(1):
            res = query(MicrobiomeAssayPrep.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield Metabolome.load_metabolome(doc)

            res_count -= len(res['results'])

            if res_count < 1:
                break

    def proteomes(self):
        """
        Returns an iterator of all Proteomes connected to this
        MicrobiomeAssayPrep.
        """
        self.logger.debug("In proteomes().")

        linkage_query = '"{}"[linkage.derived_from] && "proteome"[node_type]'.format(self.id)

        query = iHMPSession.get_session().get_osdf().oql_query

        from cutlass.Proteome import Proteome

        for page_no in count(1):
            res = query(MicrobiomeAssayPrep.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield Proteome.load_proteome(doc)

            res_count -= len(res['results'])

            if res_count < 1:
                break

    def _derived_docs(self):
        self.logger.debug("In _derived_docs.")

        linkage_query = '"{}"[linkage.derived_from]'.format(self.id)
        query = iHMPSession.get_session().get_osdf().oql_query

        for page_no in count(1):
            res = query(MicrobiomeAssayPrep.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield doc
            res_count -= len(res['results'])

            if res_count < 1:
                break

    def derivations(self):
        """
        Return an iterator of all the derived nodes from this prep, including
        lipidomes, metabolomes, cytokines, proteomes, etc...
        """
        self.logger.debug("In _derived_docs.")

        from cutlass.Cytokine import Cytokine
        from cutlass.Lipidome import Lipidome
        from cutlass.Metabolome import Metabolome
        from cutlass.Proteome import Proteome

        for doc in self._derived_docs():
            if doc['node_type'] == "cytokine":
                yield Cytokine.load_cytokine(doc)
            elif doc['node_type'] == "lipidome":
                yield Lipidome.load_lipidome(doc)
            elif doc['node_type'] == "metabolome":
                yield Metabolome.load_metabolome(doc)
            elif doc['node_type'] == "proteome":
                yield Proteome.load_proteome(doc) class HostAssayPrep(Base):
    """
    The class encapsulates iHMP host assay prep data.  It contains all
    the fields required to save a such an object in OSDF.

    Attributes:
        namespace (str): The namespace this class will use in OSDF.
    """
    namespace = "ihmp"

    def __init__(self, *args, **kwargs):
        """
        Constructor for the HostAssayPrep class. This initializes the
        fields specific to the class, and inherits from the Base class.

        Args:
            None
        """
        self.logger = logging.getLogger(self.__module__ + '.' + self.__class__.__name__)

        self.logger.addHandler(logging.NullHandler())

        self._id = None
        self._version = None
        self._links = {}
        self._tags = []

        # Required properties
        self._center = None
        self._comment = None
        self._contact = None
        self._experiment_type = None
        self._prep_id = None
        self._pride_id = None
        self._sample_name = None
        self._storage_duration = None
        self._study = None
        self._title = None

        # Optional properties
        self._cell_type = None
        self._exp_description = None
        self._protocol_name = None
        self._protocol_steps = None
        self._reference = None
        self._sample_description = None
        self._short_label = None
        self._species = None
        self._tissue = None
        self._urls = None

        super(HostAssayPrep, self).__init__(*args, **kwargs)

    @property
    def comment(self):
        """
        str: A descriptive comment for the prep.
        """
        self.logger.debug("In 'comment' getter.")

        return self._comment

    @comment.setter
    @enforce_string
    def comment(self, comment):
        """
        The setter for a descriptive comment for the prep object.

        Args:
            comment (str): The comment text.

        Returns:
            None
        """
        self.logger.debug("In 'comment' setter.")

        self._comment = comment

    @property
    def pride_id(self):
        """
        str: PRIDE identifier corresponding to study.
        """
        self.logger.debug("In 'pride_id' getter.")

        return self._pride_id

    @pride_id.setter
    @enforce_string
    def pride_id(self, pride_id):
        """
        The setter for the PRIDE identifier corresponding to study.

        Args:
            pride_id (str): The PRIDE identifier

        Returns:
            None
        """
        self.logger.debug("In 'pride_id' setter.")

        self._pride_id = pride_id

    @property
    def sample_name(self):
        """
        str: The short label that is referable to the sample used to
        generate the dataset.
        """
        self.logger.debug("In 'sample_name' getter.")

        return self._sample_name

    @sample_name.setter
    @enforce_string
    def sample_name(self, sample_name):
        """
        The setter for the short label that is referable to the sample
        used to generate the dataset.

        Args:
            sample_name (str): Short label for the sample.

        Returns:
            None
        """
        self.logger.debug("In 'sample_name' setter.")

        self._sample_name = sample_name

    @property
    def title(self):
        """
        str: The description of the particular experiment.
        """
        self.logger.debug("In 'title' getter.")

        return self._title

    @title.setter
    @enforce_string
    def title(self, title):
        """
        The setter for the description of the particular experiment.

        Args:
            title (str): Experiment title

        Returns:
            None
        """
        self.logger.debug("In 'title' setter.")

        self._title = title

    @property
    def short_label(self):
        """
        str: The short label/nomenclature used to group/organize experiments.
        """
        self.logger.debug("In 'short_label' getter.")

        return self._short_label

    @short_label.setter
    @enforce_string
    def short_label(self, short_label):
        """
        Set the short label/nomenclature used to group/organize experiments.

        Args:
            short_label (str): Short label used to group experiments

        Returns:
            None
        """
        self.logger.debug("In 'short_label' setter.")

        self._short_label = short_label

    @property
    def center(self):
        """
        str: The center responsible for generating the microbiome assay Prep.
        """
        self.logger.debug("In 'center' getter.")

        return self._center

    @center.setter
    @enforce_string
    def center(self, center):
        """
        Set the center responsible for generating the microbiome assay prep.

        Args:
            center (str): The center responsible for generating the
            microbiome assay prep.

        Returns:
            None
        """
        self.logger.debug("In 'center' setter.")

        self._center = center

    @property
    def contact(self):
        """
        str: Get the name and email of the primary contact at the center.
        """
        self.logger.debug("In 'contact' getter.")

        return self._contact

    @contact.setter
    @enforce_string
    def contact(self, contact):
        """
        Set the name and email of the primary contact at the center.

        Args:
            contact (str): Name and email of the primary contact at the center.

        Returns:
            None
        """
        self.logger.debug("In 'contact' setter.")

        self._contact = contact

    @property
    def prep_id(self):
        """
        str: Get the internal assay prep ID.
        """
        self.logger.debug("In 'prep_id' getter.")

        return self._prep_id

    @prep_id.setter
    @enforce_string
    def prep_id(self, prep_id):
        """
        Set the internal assay prep ID.

        Args:
            prep_id (str): Internal assay prep ID.

        Returns:
            None
        """
        self.logger.debug("In 'prep_id' setter.")

        self._prep_id = prep_id

    @property
    def storage_duration(self):
        """
        str: Get the MIGS/MIMS storage duration in days.
        """
        self.logger.debug("In 'storage_duration' getter.")

        return self._storage_duration

    @storage_duration.setter
    @enforce_int
    def storage_duration(self, storage_duration):
        """
        Set the MIGS/MIMS storage duration in days.

        Args:
            storage_duration (int): Storage duration in days.

        Returns:
            None
        """
        self.logger.debug("In 'storage_duration' setter.")

        self._storage_duration = storage_duration

    @property
    def experiment_type(self):
        """
        str: Get the PRIDE experiment type.
        """
        self.logger.debug("In 'experiment_type' getter.")

        return self._experiment_type

    @experiment_type.setter
    @enforce_string
    def experiment_type(self, experiment_type):
        """
        Set the PRIDE experiment type.

        Args:
            experiment_type (str): Experiment type, as defined by PRIDE.

        Returns:
            None
        """
        self.logger.debug("In 'experiment_type' setter.")

        self._experiment_type = experiment_type

    @property
    def species(self):
        """
        str: Controlled vocabulary term to describe a single species. NEWT CV
        terms are allowed.
        """
        self.logger.debug("In 'species' getter.")

        return self._species

    @species.setter
    @enforce_string
    def species(self, species):
        """
        Controlled vocabulary term to describe a single species. NEWT CV
        terms are allowed.

        Args:
            species (str): Term to describe a single species.

        Returns:
            None
        """
        self.logger.debug("In 'species' setter.")

        self._species = species

    @property
    def cell_type(self):
        """
        str: Controlled vocabulary term to describe a single cell type. Cell
        type ontology CV terms are allowed.
        """
        self.logger.debug("In 'cell_type' getter.")

        return self._cell_type

    @cell_type.setter
    @enforce_string
    def cell_type(self, cell_type):
        """
        Controlled vocabulary term to describe a single cell type. Cell
        type ontology CV terms are allowed.

        Args:
            cell_type (str): Term to describe the cell type.

        Returns:
            None
        """
        self.logger.debug("In 'cell_type' setter.")

        self._cell_type = cell_type

    @property
    def tissue(self):
        """
        str: Controlled vocabulary term to describe a single tissue. BRENDA
        Tissue CV terms are allowed.
        """
        self.logger.debug("In 'tissue' getter.")

        return self._tissue

    @tissue.setter
    @enforce_string
    def tissue(self, tissue):
        """
        Controlled vocabulary term to describe a single tissue. BRENDA Tissue
        CV terms are allowed.

        Args:
            tissue (str): Term to describe the tissue.

        Returns:
            None
        """
        self.logger.debug("In 'tissue' setter.")

        self._tissue = tissue

    @property
    def urls(self):
        """
        list: List of URL strings to relevant electronic resources.
        """
        self.logger.debug("In 'urls' getter.")

        return self._urls

    @urls.setter
    def urls(self, urls):
        """
        URLs of relevant electronic resources.

        Args:
            urls (list): List of URL strings

        Returns:
            None
        """

        self._urls = urls

    @property
    def reference(self):
        """
        str: Link to literature citation for which this experiment provides
        supporting evidence.
        """
        self.logger.debug("In 'reference' getter.")

        return self._reference

    @reference.setter
    @enforce_string
    def reference(self, reference):
        """
        Set the literature citation for which this experiment provides
        supporting evidence.

        Args:
            reference (str): Supporting evidence link.

        Returns:
            None
        """
        self.logger.debug("In 'reference' setter.")

        self._reference = reference

    @property
    def protocol_name(self):
        """
        str: The protocol title with versioning.
        """
        self.logger.debug("In 'protocol_name' getter.")

        return self._protocol_name

    @protocol_name.setter
    @enforce_string
    def protocol_name(self, protocol_name):
        """
        Set the protocol title with versioning.

        Args:
            protocol_name (str): Protocol title with versioning, ideally,
            pointing to a URL.

        Returns:
            None
        """
        self.logger.debug("In 'protocol_name' setter.")

        self._protocol_name = protocol_name

    @property
    def protocol_steps(self):
        """
        str: Description of the sample processing steps.
        """
        self.logger.debug("In 'protocol_steps' getter.")

        return self._protocol_steps

    @protocol_steps.setter
    @enforce_string
    def protocol_steps(self, protocol_steps):
        """
        Set the description of the sample processing steps.

        Args:
            protocol_name (str): Protocol title with versioning, ideally,
            pointing to a URL.

        Returns:
            None
        """
        self.logger.debug("In 'protocol_steps' setter.")

        self._protocol_steps = protocol_steps

    @property
    def exp_description(self):
        """
        str: Description of the goals and objectives of this study.
        """
        self.logger.debug("In 'exp_description' getter.")

        return self._exp_description

    @exp_description.setter
    @enforce_string
    def exp_description(self, exp_description):
        """
        Set the description of the goals and objectives of this study,
        summary of the abstract, optimally 2-3 sentences.

        Args:
            exp_description (str): Description of the goals/objectives
            of the study.

        Returns:
            None
        """
        self.logger.debug("In 'exp_description' setter.")

        self._exp_description = exp_description

    @property
    def sample_description(self):
        """
        str: Expansible description of the sample used to generate the
        dataset.
        """
        self.logger.debug("In 'sample_description' getter.")

        return self._sample_description

    @sample_description.setter
    @enforce_string
    def sample_description(self, sample_description):
        """
        Set the expansible description of the sample used to generate the
        dataset

        Args:
            sample_description (str): Expansible description of the sample
            used to generate the dataset

        Returns:
            None
        """
        self.logger.debug("In 'sample_description' setter.")

        self._sample_description = sample_description

    @property
    def study(self):
        """
        str: One of the 3 studies that are part of the iHMP.
        """
        self.logger.debug("In 'study' getter.")

        return self._study

    @study.setter
    @enforce_string
    def study(self, study):
        """
        One of the 3 studies that are part of the iHMP.

        Args:
            study (str): One of the 3 studies that are part of the iHMP.

        Returns:
            None
        """
        self.logger.debug("In 'study' setter.")

        self._study = study

    def validate(self):
        """
        Validates the current object's data/JSON against the current
        schema in the OSDF instance for that specific object. All required
        fields for that specific object must be present.

        Args:
            None

        Returns:
            A list of strings, where each string is the error that the
            validation raised during OSDF validation
        """
        self.logger.debug("In validate.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        (valid, error_message) = session.get_osdf().validate_node(document)

        problems = []
        if not valid:
            self.logger.info("Validation did not succeed.")
            problems.append(error_message)

        if 'prepared_from' not in self._links.keys():
            problems.append("Must have a 'prepared_from' link to a sample.")

        self.logger.debug("Number of validation problems: %s.", len(problems))
        return problems

    def is_valid(self):
        """
        Validates the current object's data/JSON against the current schema
        in the OSDF instance for the specific object. However, unlike
        validates(), this method does not provide exact error messages,
        it states if the validation was successful or not.

        Args:
            None

        Returns:
            True if the data validates, False if the current state of
            fields in the instance do not validate with the OSDF instance
        """
        self.logger.debug("In is_valid.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        (valid, _error_message) = session.get_osdf().validate_node(document)

        if 'prepared_from' not in self._links.keys():
            valid = False

        self.logger.debug("Valid? %s", str(valid))

        return valid

    def _get_raw_doc(self):
        """
        Generates the raw JSON document for the current object. All required
        fields are filled into the JSON document, regardless they are set or
        not. Any remaining fields are included only if they are set. This
        allows the user to visualize the JSON to ensure fields are set
        appropriately before saving into the database.

        Args:
            None

        Returns:
            An object representation of the JSON document.
        """
        self.logger.debug("In _get_raw_doc.")

        prep_doc = {
            'acl': {
                'read': ['all'],
                'write': [HostAssayPrep.namespace]
            },
            'linkage': self._links,
            'ns': HostAssayPrep.namespace,
            'node_type': 'host_assay_prep',
            'meta': {
                'comment': self._comment,
                'sample_name': self._sample_name,
                'title': self._title,
                'center': self._center,
                'contact': self._contact,
                'prep_id': self._prep_id,
                'experiment_type': self._experiment_type,
                'subtype': self._study,
                'study': self._study,
                'tags': self._tags
            }
        }

        if self._id is not None:
            self.logger.debug("%s object has the OSDF id set.", __name__)
            prep_doc['id'] = self._id

        if self._version is not None:
            self.logger.debug("%s object has the OSDF version set.", __name__)
            prep_doc['ver'] = self._version

        # Handle HostAssayPrep optional properties
        if self._short_label is not None:
            self.logger.debug("%s object has the 'short_label' property set.", __name__)
            prep_doc['meta']['short_label'] = self._short_label

        if self._urls is not None:
            self.logger.debug("%s object has the 'url' property set.", __name__)
            prep_doc['meta']['urls'] = self._urls

        if self._pride_id is not None:
            self.logger.debug("%s object has the 'pride_id' property set.", __name__)
            prep_doc['meta']['pride_id'] = self._pride_id

        if self._species is not None:
            self.logger.debug("%s object has the 'species' property set.", __name__)
            prep_doc['meta']['species'] = self._species

        if self._cell_type is not None:
            self.logger.debug("%s object has the 'cell_type' property set.", __name__)
            prep_doc['meta']['cell_type'] = self._cell_type

        if self._tissue is not None:
            self.logger.debug("%s object has the 'tissue' property set.", __name__)
            prep_doc['meta']['tissue'] = self._tissue

        if self._protocol_name is not None:
            self.logger.debug("%s object has the 'protocol_name' property set.", __name__)
            prep_doc['meta']['protocol_name'] = self._protocol_name

        if self._protocol_steps is not None:
            self.logger.debug("%s object has the 'protocol_steps' property set.", __name__)
            prep_doc['meta']['protocol_steps'] = self._protocol_steps

        if self._reference is not None:
            self.logger.debug("%s object has the 'reference' property set.", __name__)
            prep_doc['meta']['reference'] = self._reference

        if self._exp_description is not None:
            self.logger.debug("%s object has the 'exp_description' property set.", __name__)
            prep_doc['meta']['exp_description'] = self._exp_description

        if self._sample_description is not None:
            self.logger.debug("%s object has the 'sample_description' property set.", __name__)
            prep_doc['meta']['sample_description'] = self._sample_description

        if self._storage_duration is not None:
            self.logger.debug("%s object has the 'storage_duration' property set.", __name__)
            prep_doc['meta']['storage_duration'] = self._storage_duration

        return prep_doc

    @staticmethod
    def required_fields():
        """
        A static method. The required fields for the class.

        Args:
            None
        Returns:
            Tuple of strings for required properties.
        """
        module_logger.debug("In required fields.")
        return ("comment", "sample_name", "title", "center", "contact",
                "prep_id", "experiment_type", "study", "tags")

    def delete(self):
        """
        Deletes the current object (self) from OSDF. If the object has not been
        previously saved (node ID is not set), then an error message will be
        logged stating the object was not deleted. If the ID is set, and exists
        in the OSDF instance, then the object will be deleted from the OSDF
        instance, and this object must be re-saved in order to use it again.

        Args:
            None

        Returns:
            True upon successful deletion, False otherwise.
        """
        self.logger.debug("In delete.")

        if self._id is None:
            self.logger.warn("Attempt to delete a %s with no ID.", __name__)
            raise Exception("%s does not have an ID." % __name__)

        host_prep_id = self._id

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        # Assume failure
        success = False

        try:
            self.logger.info("Deleting %s with ID %s.", __name__, host_prep_id)
            session.get_osdf().delete_node(host_prep_id)
            success = True
        except Exception as delete_exception:
            self.logger.exception(delete_exception)
            self.logger.error("An error occurred when deleting %s.", self)

        return success

    @staticmethod
    def search(query="\"host_assay_prep\"[node_type]"):
        """
        Searches OSDF for HostAssayPrep nodes. Any criteria the user wishes to
        add is provided by the user in the query language specifications
        provided in the OSDF documentation. A general format is (including the
        quotes and brackets):

        "search criteria"[field to search]

        If there are any results, they are returned as HostAssayPrep instances,
        otherwise an empty list will be returned.

        Args:
            query (str): The query for the OSDF framework. Defaults to the
                         HostAssayPrep node type.

        Returns:
            Returns an array of HostAssayPrep objects. It returns an empty list
            if there are no results.
        """
        module_logger.debug("In search.")

        session = iHMPSession.get_session()
        module_logger.info("Got iHMP session.")

        if query != '"host_assay_prep"[node_type]':
            query = '({}) && "host_assay_prep"[node_type]'.format(query)

        module_logger.debug("Submitting OQL query: %s", query)

        prep_data = session.get_osdf().oql_query(HostAssayPrep.namespace, query)

        all_results = prep_data['results']

        result_list = list()

        if len(all_results) > 0:
            for result in all_results:
                prep_result = HostAssayPrep.load_host_assay_prep(result)
                result_list.append(prep_result)

        return result_list

    @staticmethod
    def load_host_assay_prep(prep_data):
        """
        Takes the provided JSON string and converts it to a
        HostAssayPrep object

        Args:
            prep_data (str): The JSON string to convert

        Returns:
            Returns a HostAssayPrep instance.
        """
        module_logger.info("Creating a template %s.", __name__)
        prep = HostAssayPrep()

        module_logger.debug("Filling in %s details.", __name__)
        prep._set_id(prep_data['id'])
        prep.links = prep_data['linkage']
        prep.version = prep_data['ver']

        # Required fields
        prep.comment = prep_data['meta']['comment']
        prep.contact = prep_data['meta']['contact']
        prep.center = prep_data['meta']['center']
        prep.experiment_type = prep_data['meta']['experiment_type']
        prep.prep_id = prep_data['meta']['prep_id']
        prep.sample_name = prep_data['meta']['sample_name']
        prep.study = prep_data['meta']['study']
        prep.tags = prep_data['meta']['tags']
        prep.title = prep_data['meta']['title']

        # Optional fields
        if 'short_label' in prep_data['meta']:
            prep.short_label = prep_data['meta']['short_label']

        if 'urls' in prep_data['meta']:
            prep.urls = prep_data['meta']['urls']

        if 'pride_id' in prep_data['meta']:
            prep.pride_id = prep_data['meta']['pride_id']

        if 'species' in prep_data['meta']:
            prep.species = prep_data['meta']['species']

        if 'cell_type' in prep_data['meta']:
            prep.cell_type = prep_data['meta']['cell_type']

        if 'tissue' in prep_data['meta']:
            prep.tissue = prep_data['meta']['tissue']

        if 'reference' in prep_data['meta']:
            prep.reference = prep_data['meta']['reference']

        if 'protocol_name' in prep_data['meta']:
            prep.protocol_name = prep_data['meta']['protocol_name']

        if 'protocol_steps' in prep_data['meta']:
            prep.protocol_steps = prep_data['meta']['protocol_steps']

        if 'exp_description' in prep_data['meta']:
            prep.exp_description = prep_data['meta']['exp_description']

        if 'sample_description' in prep_data['meta']:
            prep.sample_description = prep_data['meta']['sample_description']

        if 'storage_duration' in prep_data['meta']:
            prep.storage_duration = prep_data['meta']['storage_duration']

        module_logger.debug("Returning loaded %s.", __name__)
        return prep

    @staticmethod
    def load(prep_id):
        """
        Loads the data for the specified input ID from the OSDF instance to
        this object.  If the provided ID does not exist, then an error message
        is provided stating the project does not exist.

        Args:
            prep_id (str): The OSDF ID for the document to load.

        Returns:
            A HostAssayPrep object with all the available OSDF data loaded
            into it.
        """
        module_logger.debug("In load. Specified ID: %s", prep_id)

        session = iHMPSession.get_session()
        module_logger.info("Got iHMP session.")
        prep_data = session.get_osdf().get_node(prep_id)
        prep = HostAssayPrep.load_host_assay_prep(prep_data)

        module_logger.debug("Returning loaded %s.", __name__)

        return prep

    def save(self):
        """
        Saves the data in OSDF. The JSON form of the current data for the
        instance is validated in the save function. If the data is not valid,
        then the data will not be saved. If the instance was saved previously,
        then the node ID is assigned the alpha numeric found in the OSDF
        instance. If not saved previously, then the node ID is 'None', and upon
        a successful, will be assigned to the alpha numeric ID found in OSDF.
        Also, the version is updated as the data is saved in OSDF.

        Args:
            None

        Returns;
            True if successful, False otherwise.

        """
        self.logger.debug("In save.")

        # If node previously saved, use edit_node instead since ID
        # is given (an update in a way)
        # can also use get_node to check if the node already exists
        if not self.is_valid():
            self.logger.error("Cannot save, data is invalid.")
            return False

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        osdf = session.get_osdf()

        success = False

        if self._id is None:
            self.logger.info("About to insert a new %s OSDF node.", __name__)

            # Get the JSON form of the data and load it
            self.logger.debug("Converting %s to parsed JSON form.", __name__)
            data = json.loads(self.to_json())

            try:
                node_id = osdf.insert_node(data)

                self._set_id(node_id)
                self._version = 1
                success = True
            except Exception as save_exception:
                self.logger.exception(save_exception)
                self.logger.error("An error occurred when saving %s.", self)
        else:
            self.logger.info("%s already has an ID, so we do an update (not an insert).",
                             __name__)

            try:
                prep_data = self._get_raw_doc()
                self.logger.info("%s already has an ID, so we do an update (not an insert).",
                                 __name__
                                )
                prep_id = self._id
                self.logger.debug("%s OSDF ID to update: %s.", __name__, prep_id)
                osdf.edit_node(prep_data)

                prep_data = osdf.get_node(prep_id)
                latest_version = prep_data['ver']

                self.logger.debug("The version of this %s is now: %s",
                                  __name__,
                                  str(latest_version)
                                 )
                self._version = latest_version
                success = True
            except Exception as update_exception:
                self.logger.exception(update_exception)
                self.logger.error("An error occurred when updating %s.", self)

        return success

    def cytokines(self):
        """
        Returns an iterator of all Cytokines connected to this HostAssayPrep.
        """
        self.logger.debug("In cytokines().")

        linkage_query = '"{}"[linkage.derived_from] and "cytokine"[node_type]'.format(self.id)

        query = iHMPSession.get_session().get_osdf().oql_query

        from cutlass.Cytokine import Cytokine

        for page_no in count(1):
            res = query(HostAssayPrep.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield Cytokine.load_cytokine(doc)

            res_count -= len(res['results'])

            if res_count < 1:
                break

    def lipidomes(self):
        """
        Returns an iterator of all Lipidomes connected to this HostAssayPrep.
        """
        self.logger.debug("In lipidomes().")

        linkage_query = '"{}"[linkage.derived_from] and "lipidome"[node_type]'.format(self.id)

        query = iHMPSession.get_session().get_osdf().oql_query

        from cutlass.Lipidome import Lipidome

        for page_no in count(1):
            res = query(HostAssayPrep.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield Lipidome.load_lipidome(doc)

            res_count -= len(res['results'])

            if res_count < 1:
                break

    def metabolomes(self):
        """
        Returns an iterator of all Metabolomes connected to this HostAssayPrep.
        """
        self.logger.debug("In metabolomes().")

        linkage_query = '"{}"[linkage.derived_from] and "metabolome"[node_type]'.format(self.id)

        query = iHMPSession.get_session().get_osdf().oql_query

        from cutlass.Metabolome import Metabolome

        for page_no in count(1):
            res = query(HostAssayPrep.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield Metabolome.load_metabolome(doc)

            res_count -= len(res['results'])

            if res_count < 1:
                break

    def proteomes(self):
        """
        Returns an iterator of all Proteomes connected to this HostAssayPrep.
        """
        self.logger.debug("In proteomes().")

        linkage_query = '"{}"[linkage.derived_from] and "proteome"[node_type]'.format(self.id)

        query = iHMPSession.get_session().get_osdf().oql_query

        from cutlass.Proteome import Proteome

        for page_no in count(1):
            res = query(HostAssayPrep.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield Proteome.load_proteome(doc)

            res_count -= len(res['results'])

            if res_count < 1:
                break

    def _derived_docs(self):
        self.logger.debug("In _derived_docs().")

        linkage_query = '"{}"[linkage.derived_from]'.format(self.id)
        query = iHMPSession.get_session().get_osdf().oql_query

        for page_no in count(1):
            res = query(HostAssayPrep.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield doc
            res_count -= len(res['results'])

            if res_count < 1:
                break

    def derivations(self):
        """
        Return an iterator of all the derived nodes from this prep, including
        lipidomes, metabolomes, cytokines, etc...
        """
        self.logger.debug("In derivations().")

        from cutlass.Cytokine import Cytokine
        from cutlass.Lipidome import Lipidome
        from cutlass.Metabolome import Metabolome
        from cutlass.Proteome import Proteome

        for doc in self._derived_docs():
            if doc['node_type'] == "lipidome":
                yield Lipidome.load_lipidome(doc)
            elif doc['node_type'] == "metabolome":
                yield Metabolome.load_metabolome(doc)
            elif doc['node_type'] == "cytokine":
                yield Cytokine.load_cytokine(doc)
            elif doc['node_type'] == "proteome":
                yield Proteome.load_proteome(doc) class SampleAttribute(Base):
    """
    The class encapsulates iHMP sample attribute data. It contains all
    the fields required to save a such an object in OSDF.

    Attributes:
        namespace (str): The namespace this class will use in OSDF.
    """
    namespace = "ihmp"

    def __init__(self, *args, **kwargs):
        """
        Constructor for the SampleAttribute class. This initializes the
        fields specific to the class, and inherits from the Base class.

        Args:
            None
        """
        self.logger = logging.getLogger(self.__module__ + '.' + self.__class__.__name__)

        self.logger.addHandler(logging.NullHandler())

        self._id = None
        self._version = None
        self._links = {}
        self._tags = []

        # Required properties
        self._fecalcal = None
        self._study = None

        # Optional properties
        self._sample_desc = None
        self._sample_type = None
        self._subproject = None

        super(SampleAttribute, self).__init__(*args, **kwargs)

    @property
    def fecalcal(self):
        """
        str: FecalCal result.
        """
        self.logger.debug("In 'fecalcal' getter.")
        return self._fecalcal

    @fecalcal.setter
    @enforce_string
    def fecalcal(self, fecalcal):
        """
        FecalCal result.

        Args:
            fecalcal (str): FecalCal result.

        Returns:
            None
        """
        self.logger.debug("In 'fecalcal' setter.")

        self._fecalcal = fecalcal

    @property
    def sample_desc(self):
        """
        str: Optional sample description property to indicate sample source/omic type.
        """
        self.logger.debug("In 'sample_desc' getter.")
        return self._sample_desc

    @sample_desc.setter
    @enforce_string
    def sample_desc(self, sample_desc):
        """
        Optional sample description to indicate sample source/omic type.

        Args:
            sample_desc (str): The sample description.

        Returns:
            None
        """
        self.logger.debug("In 'sample_desc' setter.")

        self._sample_desc = sample_desc

    @property
    def sample_type(self):
        """
        str: Optional sample type property.
        """
        self.logger.debug("In 'sample_type' getter.")
        return self._sample_type

    @sample_type.setter
    @enforce_string
    def sample_type(self, sample_type):
        """
        The sample type.

        Args:
            sample_type (str): The sample type.

        Returns:
            None
        """
        self.logger.debug("In 'sample_type' setter.")

        self._sample_type = sample_type

    @property
    def study(self):
        """
        str: One of the 3 studies that are part of the iHMP.
        """
        self.logger.debug("In 'study' getter.")
        return self._study

    @study.setter
    @enforce_string
    def study(self, study):
        """
        One of the 3 studies that are part of the iHMP.

        Args:
            study (str): One of the 3 studies that are part of the iHMP.

        Returns:
            None
        """
        self.logger.debug("In 'study' setter.")

        self._study = study

    @property
    def subproject(self):
        """
        str: Optional subproject the sample belongs to, currently limited to
        VCU but controlled vocabulary could be expanded to include more
        projects.
        """
        self.logger.debug("In 'subproject' getter.")
        return self._subproject

    @subproject.setter
    @enforce_string
    def subproject(self, subproject):
        """
        The sample subproject.

        Args:
            subproject (str): The subproject that the sample belongs to.

        Returns:
            None
        """
        self.logger.debug("In 'subproject' setter.")

        self._subproject = subproject

    def validate(self):
        """
        Validates the current object's data/JSON against the current
        schema in the OSDF instance for that specific object. All required
        fields for that specific object must be present.

        Args:
            None

        Returns:
            A list of strings, where each string is the error that the
            validation raised during OSDF validation
        """
        self.logger.debug("In validate.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        (valid, error_message) = session.get_osdf().validate_node(document)

        problems = []
        if not valid:
            self.logger.info("Validation did not succeed.")
            problems.append(error_message)

        if 'associated_with' not in self._links.keys():
            problems.append("Must have a 'associated_with' link to a sample.")

        self.logger.debug("Number of validation problems: %s.", len(problems))

        return problems

    def is_valid(self):
        """
        Validates the current object's data/JSON against the current schema
        in the OSDF instance for the specific object. However, unlike
        validates(), this method does not provide exact error messages,
        it states if the validation was successful or not.

        Args:
            None

        Returns:
            True if the data validates, False if the current state of
            fields in the instance do not validate with the OSDF instance
        """
        self.logger.debug("In is_valid.")

        document = self._get_raw_doc()

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        (valid, _error_message) = session.get_osdf().validate_node(document)

        if 'associated_with' not in self._links.keys():
            valid = False

        self.logger.debug("Valid? %s", str(valid))

        return valid

    def _get_raw_doc(self):
        """
        Generates the raw JSON document for the current object. All required
        fields are filled into the JSON document, regardless they are set or
        not. Any remaining fields are included only if they are set. This
        allows the user to visualize the JSON to ensure fields are set
        appropriately before saving into the database.

        Args:
            None

        Returns:
            An object representation of the JSON document.
        """
        self.logger.debug("In _get_raw_doc.")

        attrib_doc = {
            'acl': {
                'read': ['all'],
                'write': [SampleAttribute.namespace]
            },
            'linkage': self._links,
            'ns': SampleAttribute.namespace,
            'node_type': 'sample_attr',
            'meta': {
                'fecalcal': self._fecalcal,
                'study': self._study,
                'subtype': self._study,
                'tags': self._tags
            }
        }

        if self._id is not None:
            self.logger.debug("%s object has the OSDF id set.", __name__)
            attrib_doc['id'] = self._id

        if self._version is not None:
            self.logger.debug("%s object has the OSDF version set.", __name__)
            attrib_doc['ver'] = self._version

        # Handle SampleAttribute optional properties
        if self.sample_desc is not None:
            self.logger.debug("%s object has the 'sample_desc' property set.", __name__)
            attrib_doc['meta']['sample_desc'] = self.sample_desc

        if self.sample_type is not None:
            self.logger.debug("%s object has the 'sample_type' property set.", __name__)
            attrib_doc['meta']['sample_type'] = self.sample_type

        if self.subproject is not None:
            self.logger.debug("%s object has the 'subproject' property set.", __name__)
            attrib_doc['meta']['subproject'] = self.subproject

        return attrib_doc

    @staticmethod
    def required_fields():
        """
        A static method. The required fields for the class.

        Args:
            None
        Returns:
            Tuple of strings of required properties.
        """
        module_logger.debug("In required_fields.")

        return ("fecalcal", "study", "tags")

    def delete(self):
        """
        Deletes the current object (self) from OSDF. If the object has not been
        previously saved (node ID is not set), then an error message will be
        logged stating the object was not deleted. If the ID is set, and exists
        in the OSDF instance, then the object will be deleted from the OSDF
        instance, and this object must be re-saved in order to use it again.

        Args:
            None

        Returns:
            True upon successful deletion, False otherwise.
        """
        self.logger.debug("In delete.")

        if self._id is None:
            self.logger.warn("Attempt to delete a %s with no ID.", __name__)
            raise Exception(__name__ + " does not have an ID.")

        attrib_id = self._id

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        # Assume failure
        success = False

        try:
            self.logger.info("Deleting %s with ID %s.", __name__, attrib_id)
            session.get_osdf().delete_node(attrib_id)
            success = True
        except Exception as delete_exception:
            self.logger.error("An error occurred when deleting %s: %s",
                              self,
                              delete_exception
                             )

        return success

    @staticmethod
    def search(query="\"sample_attr\"[node_type]"):
        """
        Searches OSDF for SampleAttribute nodes. Any criteria the user wishes to
        add is provided by the user in the query language specifications
        provided in the OSDF documentation. A general format is (including the
        quotes and brackets):

        "search criteria"[field to search]

        If there are any results, they are returned as SampleAttribute instances,
        otherwise an empty list will be returned.

        Args:
            query (str): The query for the OSDF framework. Defaults to the
                         SampleAttribute node type.

        Returns:
            Returns an array of SampleAttribute objects. It returns an empty
            list if there are no results.
        """
        module_logger.debug("In search.")

        # Searching without any parameters will return all different results
        session = iHMPSession.get_session()
        module_logger.info("Got iHMP session.")

        if query != '"sample_attr"[node_type]':
            query = '({}) && "sample_attr"[node_type]'.format(query)

        module_logger.debug("Submitting OQL query: %s", query)

        attrib_data = session.get_osdf().oql_query(SampleAttribute.namespace, query)

        all_results = attrib_data['results']

        result_list = list()

        if len(all_results) > 0:
            for result in all_results:
                attrib_result = SampleAttribute.load_sample_attr(result)
                result_list.append(attrib_result)

        return result_list

    @staticmethod
    def load_sample_attr(attrib_data):
        """
        Takes the provided JSON string and converts it to a
        SampleAttribute object

        Args:
            attrib_data (str): The JSON string to convert

        Returns:
            Returns a SampleAttribute instance.
        """
        module_logger.info("Creating a template %s.", __name__)
        attrib = SampleAttribute()

        module_logger.debug("Filling in %s details.", __name__)
        attrib._set_id(attrib_data['id'])
        attrib.links = attrib_data['linkage']
        attrib.version = attrib_data['ver']

        # Required fields
        attrib.fecalcal = attrib_data['meta']['fecalcal']
        attrib.study = attrib_data['meta']['study']
        attrib.tags = attrib_data['meta']['tags']

        # Handle SampleAttribute optional properties
        if 'sample_desc' in attrib_data['meta']:
            attrib.sample_desc = attrib_data['meta']['sample_desc']

        if 'sample_type' in attrib_data['meta']:
            attrib.sample_type = attrib_data['meta']['sample_type']

        if 'subproject' in attrib_data['meta']:
            attrib.subproject = attrib_data['meta']['subproject']

        module_logger.debug("Returning loaded %s.", __name__)

        return attrib

    @staticmethod
    def load(attrib_id):
        """
        Loads the data for the specified ID from the OSDF instance to
        this object. If the provided ID does not exist, then an error message
        is provided stating the object does not exist.

        Args:
            attrib_id (str): The OSDF ID for the document to load.

        Returns:
            A SampleAttribute object with all the available OSDF data loaded
            into it.
        """
        module_logger.debug("In load. Specified ID: %s", attrib_id)

        session = iHMPSession.get_session()
        module_logger.info("Got iHMP session.")
        attrib_data = session.get_osdf().get_node(attrib_id)
        attrib = SampleAttribute.load_sample_attr(attrib_data)

        module_logger.debug("Returning loaded %s.", __name__)
        return attrib

    def save(self):
        """
        Saves the data in OSDF. The JSON form of the current data for the
        instance is validated in the save function. If the data is not valid,
        then the data will not be saved. If the instance was saved previously,
        then the node ID is assigned the alpha numeric found in the OSDF
        instance. If not saved previously, then the node ID is 'None', and upon
        a successful, will be assigned to the alpha numeric ID found in OSDF.
        Also, the version is updated as the data is saved in OSDF.

        Args:
            None

        Returns;
            True if successful, False otherwise.

        """
        self.logger.debug("In save.")

        # If node previously saved, use edit_node instead since ID
        # is given (an update in a way)
        # can also use get_node to check if the node already exists
        if not self.is_valid():
            self.logger.error("Cannot save, data is invalid.")
            return False

        session = iHMPSession.get_session()
        self.logger.info("Got iHMP session.")

        osdf = session.get_osdf()

        success = False

        if self._id is None:
            self.logger.info("About to insert a new %s OSDF node.", __name__)

            # Get the JSON form of the data and load it
            self.logger.debug("Converting %s to parsed JSON form.", __name__)
            data = json.loads(self.to_json())

            try:
                node_id = osdf.insert_node(data)

                self._set_id(node_id)
                self._version = 1
                success = True
            except Exception as save_exception:
                self.logger.error("An error occurred when saving %s.", save_exception)
        else:
            self.logger.info("%s already has an ID, so we " + \
                             "do an update (not an insert).", __name__)

            try:
                attrib_data = self._get_raw_doc()
                self.logger.info("%s already has an ID, " + \
                                 "so we do an update (not an insert).", __name__)
                attrib_id = self._id
                self.logger.debug("%s OSDF ID to update: %s.", __name__, attrib_id)
                osdf.edit_node(attrib_data)

                attrib_data = osdf.get_node(attrib_id)
                latest_version = attrib_data['ver']

                self.logger.debug("The version of this %s is " + \
                                  "now: %s", __name__, str(latest_version))
                self._version = latest_version
                success = True
            except Exception as edit_exception:
                self.logger.error("An error occurred when updating %s.", edit_exception)

        return success def enforce_dict(func):
    """
    Decorator to enforce a dict argument.
    """
    def wrapper(self, *args):
        if type(args[0]) is not dict:
            raise ValueError("Invalid type provided. Must be a dict.")

        func(self, *args)

    return wrapper def enforce_string(func):
    """
    Decorator to enforce a string argument.
    """
    def wrapper(self, *args):
        if type(args[0]) is not str:
            raise ValueError("Invalid type provided. Must be a string.")

        func(self, *args)

    return wrapper
import logging
from itertools import count
from cutlass.iHMPSession import iHMPSession
from cutlass.mixs import MIXS, MixsException
from cutlass.Base import Base
from cutlass.WgsDnaPrep import WgsDnaPrep
from cutlass.SixteenSDnaPrep import SixteenSDnaPrep
from cutlass.HostSeqPrep import HostSeqPrep
from cutlass.MicrobiomeAssayPrep import MicrobiomeAssayPrep
from cutlass.HostAssayPrep import HostAssayPrep
from cutlass.SampleAttribute import SampleAttribute
from cutlass.Util import enforce_dict, enforce_string
            },
            'linkage': self._links,
            'ns': Sample.namespace,
            'node_type': 'sample',
            'meta': {
                'fma_body_site': self._fma_body_site,
                'mixs': self._mixs,
                'subtype': "sample",
                'tags': self._tags
            }
        }

        if self._id is not None:
            self.logger.debug("%s object has the OSDF id set.", __name__)
            sample_doc['id'] = self._id

        if self._version is not None:
            self.logger.debug("%s object has the OSDF version set.", __name__)
            sample_doc['ver'] = self._version

        # Handle Sample optional properties
        if self._body_site is not None:
            self.logger.debug("%s object has the 'body_site' property set.", __name__)
            sample_doc['meta']['body_site'] = self._body_site

        if self._int_sample_id is not None:
            self.logger.debug("%s object has the 'int_sample_id' property set.", __name__)
            sample_doc['meta']['name'] = self._name

        if self._name is not None:
            self.logger.debug("%s object has the 'name' property set.", __name__)
            sample_doc['meta']['name'] = self._name

        if self._supersite is not None:
            self.logger.debug("%s object has the 'supersite' property set.", __name__)
            sample_doc['meta']['supersite'] = self._supersite
            sample_doc['meta']['subtype'] = self._supersite

        return sample_doc

    def _dep_docs(self):
        self.logger.debug("In _dep_docs().")

        linkage_query = '"{}"[linkage.prepared_from]'.format(self.id)
        query = iHMPSession.get_session().get_osdf().oql_query

        for page_no in count(1):
            res = query(Sample.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield doc
            res_count -= len(res['results'])

            if res_count < 1:
                break

    def _sample_attr_docs(self):
        linkage_query = '"{}"[linkage.associated_with]'.format(self.id)
        query = iHMPSession.get_session().get_osdf().oql_query

        for page_no in count(1):
            res = query(Sample.namespace, linkage_query, page=page_no)
            res_count = res['result_count']

            for doc in res['results']:
                yield doc
            res_count -= len(res['results'])

            if res_count < 1:
                break

    def sampleAttributes(self):
        """
        Return an iterator of the sample attributes associated with this sample.
        """
        self.logger.debug("In sampleAttributes().")

        for doc in self._sample_attr_docs():
            if doc['node_type'] == "sample_attr":
                yield SampleAttribute.load_sample_attr(doc)

    def sixteenSDnaPreps(self):
        """
        Return an iterator of the 16S DNA preps prepared from this sample.
        """
        self.logger.debug("In sixteenSDnaPreps().")

        for doc in self._dep_docs():
            if doc['node_type'] == "16s_dna_prep":
                yield SixteenSDnaPrep.load_sixteenSDnaPrep(doc)

    def hostSeqPreps(self):
        """
        Return an iterator of the HostSeqPreps prepared from this sample.
        """
        self.logger.debug("In hostSeqPreps().")

        for doc in self._dep_docs():
            if doc['node_type'] == "host_seq_prep":
                yield HostSeqPrep.load_host_seq_prep(doc)

    def microbAssayPreps(self):
        """
        Return an iterator of the MicrobiomeAssayPreps prepared from this sample.
        """
        self.logger.debug("In microbAssayPreps().")

        for doc in self._dep_docs():
            if doc['node_type'] == "microb_assay_prep":
                yield MicrobiomeAssayPrep.load_microassayprep(doc)

    def hostAssayPreps(self):
        """
        Return an iterator of the HostAssayPreps prepared from this sample.
        """
        self.logger.debug("In hostAssayPreps().")

        for doc in self._dep_docs():
            if doc['node_type'] == "host_assay_prep":

                yield HostAssayPrep.load_host_assay_prep(doc)