# -*- coding: utf-8 -*-
#
# Sample-related models.
#
# ------------------------------------------------
# imports
# -------
from enum import Enum
import json
import os
import warnings
import esp
from ..utils import normalize_resource_link_type, convert_date_value
try:
    from urllib.parse import quote_plus
except ImportError:
    from urllib import quote_plus
import dateparser
from gems import cached, composite
import six
from .. import base
from .. import utils
from .__base__ import BaseModel, LinkedModel, create_and_return_uuid, export_fixed_id, cached_uuid
from .__base__ import raw, compile_report, espfile_or_value, export_variable_definitions, push_variable_definitions
def _export_resource_link_val(r_val_value):
    resource_link_value = json.loads(r_val_value)
    link_type = resource_link_value["linkType"]
    link_type = normalize_resource_link_type(link_type)
    if not hasattr(esp.models, link_type):
        # link_type can be a pre-supplied/Out-of-box Entity Class.
        # Customers and implementations may create other entity classes like L7Product
        link_type = "Sample"
    # reference resource by fixed_id and name
    uuid = resource_link_value.pop("uuid")
    obj = getattr(esp.models, link_type)(uuid)
    if obj.fixed_id:
        resource_link_value["fixed_id"] = obj.fixed_id
    resource_link_value["name"] = obj.name
    return json.dumps(resource_link_value, default=str)
def _convert_date_value(value, resource_value):
    if not value:
        return value
    if isinstance(value, str):
        if "{{" in value:
            return value
        ret = dateparser.parse(value)
        # dateparser can return null if it can't parse value...
        if not ret:
            return value
    else:
        ret = value
    # strip time value if it isn't conveying information.
    if hasattr(ret, "hour") and ret.hour == 0 and ret.minute == 0 and hasattr(ret, "date"):
        ret = ret.date()
    ret = ret.isoformat()
    if "T" in ret and not ret.endswith("Z"):
        ret += "Z"
    base.logger.info("Converting value `{}` to `{}`".format(value, ret))
    return ret
# models
# ------
class EntityType(BaseModel):
    """
    Object for interacting with EntityTypes from the ESP database.
    See the `Usage <./usage.html>`_ and `Examples <./examples.html>`_ pages
    of the documentation for more context and comprehensive examples of
    how to create and use this type of objects.
    .. note:: Previous versions of ESP used the word ``SampleType`` instead
              of ``EntityType``. The python client now uses ``EntityType`` but
              ``SampleType``, though deprecated, is still supported for backwards
              comatibility with older code.
    Configuration:
        Simple entity type:
        .. code-block:: yaml
            name: Library
            desc: Entity type for Library
            tags: [library, demo]
        Entity type with auto-naming sequence:
        .. code-block:: yaml
            name: Library
            desc: Entity type for Library
            tags: [library, demo]
            sequences:
                - LIBRARY SEQUENCE
        Entity type with variables and sequence:
        .. code-block:: yaml
            name: Library
            desc: Entity type for Library
            tags: [library, demo]
            sequences:
                - LIBRARY SEQUENCE
            variables:
                - Entity Type:
                    rule: string
                    value: Illumina Library
                - Numeric Value:
                    rule: numeric
                    value: 0
        Create Entity type and new entities:
        .. code-block:: yaml
            name: Library
            variables:
                - Entity Type:
                    rule: string
                    value: Illumina Library
                - Numeric Value:
                    rule: numeric
                    value: 0
            create:
                - Library 1
                - name: Library 2
                  desc: My special library.
                  variables:
                    Entity Type: Non-Illumina Library
                    Numeric Value: 2
    Configuration Notes:
        * Variables specified for entities can take the same format as variables
          defined for protocols within ESP.
        * Entity creation can be nested in EntityType configuration using the
          ``create`` parameter.
        * For any new EntityType object, corresponding information
          MUST be included in the ``lab7.conf`` configuration file. Here
          are lines in that file that's relevant to the examples above:
          .. code-block:: yaml
              lims:
                auto_sample_id_format: "ESP{sample_number:06}"
                sample_id_sequences:
                  - name: "ESP SEQUENCE"
                    format: ESP{sample_number:06}
                  - name: "LIBRARY SEQUENCE"
                    format: LIB{sample_number:03}
                    sequence: library_seq
              sequences:
                lab7_sample_auto_id_seq: 1
                library_seq: 1
    Examples:
        .. code-block:: python
            >>> from esp.models import EntityType
            >>> et = EntityType('Library')
            >>> et.name, et.created_at
            ('Demo Consumable', '2019-06-21T16:04:01.199076Z')
            >>> # show relationships
            >>> et.entities
            [<Entity(name=LIB0001)>, <Entity(name=LIB0002)>, <Entity(name=LIB0003)>]
            >>> et.variables
            [{'name': 'Value 1', 'value': 1, ...}, {'name': 'Value 2', 'value': 2, ...}]
    Args:
        ident (str): Name or uuid for object.
    """
    __api__ = "sample_types"
    __api_cls__ = "SampleType"
    __version_api__ = "sample_type_definitions"
    __type_aliases__ = ["SampleType"]
    __push_format__ = {
        "meta": lambda x: {
            "lab7_id_sequence": list(x.sequences),
        },
        "resource_vars": lambda x: push_variable_definitions(raw(x.variables)),
    }
    __mutable__ = BaseModel.__mutable__ + ["resource_vars", "icon_svg", "view_template", "fixed_id"]
    __exportable__ = [
        "name",
        "tags",
        "desc",
        "sequences",
        "variables",
        "class",
        "icon_svg",
        "view_template",
        "fixed_id",
    ]
    def _export_class(self):
        if self.workflowable_class.name == "Sample":
            return ""
        return self.workflowable_class.name
    def _export_variables(self):
        return export_variable_definitions(self.variables)
    __export_format__ = {
        "sequences": lambda x: raw(x.sequences),
        "variables": _export_variables,
        "class": _export_class,
        "fixed_id": export_fixed_id,
    }
    @classmethod
    def parse_import(cls, config, overwrite=False, allow_snapshot_uuid_remap=False):
        """
        Create new object in ESP database using config file or other data.
        Args:
            config (str, dict, list): Config file or information to use in
                creating new object.
            overwrite (bool): Whether or not to delete current entry in
                the ESP database.
        """
        meta = {} if "meta" not in config else config["meta"]
        if "sequences" in config:
            meta["lab7_id_sequence"] = config["sequences"]
        else:
            meta["lab7_id_sequence"] = ["ESP SEQUENCE"]
        config["meta"] = meta
        config["resource_vars"] = push_variable_definitions(config.pop("variables", []))
        compiled = config.pop("compiled", False)
        if "view_template" in config:
            config["view_template"] = compile_report(config["view_template"], compiled)
        if "icon_svg" in config and config["icon_svg"] is not None:
            config["icon_svg"] = espfile_or_value(config["icon_svg"], True)
        if "workflowable" in config:
            config.setdefault("class", config.pop("workflowable"))
        if "class" in config:
            config["workflowable_resource_class"] = create_and_return_uuid(
                WorkflowableClass, config.pop("class"), overwrite=overwrite
            )
        cls._create = config.pop("create", [])
        return config
    @classmethod
    def parse_local_import(cls, data):
        if "icon_svg" in data:
            raise ValueError(
                "Custom icons (`icon_svg`) in EntityType definitions " "are not currently supported for local imports."
            )
        class_ = None
        if "class" in data:
            class_ = data.pop("class")
        data = cls.parse_import(data)
        if class_:
            data["workflowable_resource_class"] = cached_uuid(WorkflowableClass, class_)
        data["cls"] = cls.__api_cls__
        return data
    @classmethod
    def parse_response(cls, data, overwrite=False, prompt=False, allow_snapshot_uuid_remap=False):
        """
        Method for parsing request and returning
        :param allow_snapshot_uuid_remap:
        """
        create = cls._create
        del cls._create
        st = cls.from_data(data)
        for idx, conf in enumerate(create):
            if isinstance(conf, six.string_types):
                create[idx] = {"name": conf}
            create[idx]["sample_type"] = st
        if len(create):
            Sample.create(create, overwrite=False)
        return st
    def drop(self, deep=False):
        """
        Issue DELETE request to remove object in ESP database.
        """
        if deep:
            for obj in self.samples:
                obj.drop()
        # don't drop default generic sample type via client
        if self.name.lower() != "Generic sample".lower() and self.name.lower() != "MES Product".lower():
            super(SampleType, self).drop()
        return
    @cached
    def sequences(self):
        """
        Return sequence ID used within ESP. This id is used throughout the system
        in a variety of different ways, so being able to query it is useful.
        """
        return list(self.data["meta"].lab7_id_sequence)
    @cached
    def variables(self):
        """
        Proxy for weird API schema requiring strange 'meta' field.
        """
        return self.resource_vars
    @cached
    def workflowable_class(self):
        """
        Proxy for accessing SampleType WorkflowableClass
        """
        return WorkflowableClass(self.data.workflowable_resource_class)
    @cached
    def entities(self):
        """
        Return Sammple object associated with sample type.
        """
        result = base.SESSION.get("/api/samples?sample_type={}".format(quote_plus(self.name)))
        return [Entity.from_data(s) for s in result.json()]
    @property
    def samples(self):
        return self.entities
    def _data_by_uuid(self):
        # Work around fact that sample -> sample_type association is based
        # on the /definition/ uuid, NOT the head uuid. In general, hard to
        # know if we're getting head or definition. Try head first.
        data = super(SampleType, self)._data_by_uuid()
        if not data:
            # have to use query here b/c the permission setup on
            # get_sample_type_definition tries to resolve a
            # SampleType instead of a SampleTypeDefinition.
            res = base.SESSION.get("/api/sample_type_definitions?uuids={}".format(json.dumps([self.ident]))).json()
            if len(res) == 0:
                return {}
            return res[0]
        else:
            return data
SampleType = EntityType
class DuplicateWorkflowableTypeNames(UserWarning):
    def __init__(self, typeName, resolvedTypes, selectedType):
        resolvedTypesDisplay = [{"uuid": x["uuid"], "cls": x["cls"]} for x in resolvedTypes]
        selectedTypeDisplay = {"uuid": selectedType["uuid"], "cls": selectedType["cls"]}
        super().__init__(
            "WorkflowableTypeName `{}` is shared by more than one type:\n\t* {}\nUsing:\n\t* {}".format(
                typeName, "\n\t* ".join(str(x) for x in resolvedTypesDisplay), selectedTypeDisplay
            )
        )
class CoreWorkflowableClass(Enum):
    """
    Enum for dealing with core types the backend considers "Workflowable".
    The value determines the relative priority of the classes for situations where
    a type name resolves to more than one class. ESP 3.0.0 prevents some
    duplications but not others because we have to walk existing implementations forward
    where some type names may be deliberately duplicated since ESP < 3.0.0 didn't support
    Containers and Items as workflowable types.
    Note:
        This enum differs from the "WorkflowableClass" in that these three classes are back-end
        objects. All user-defined workflowable classes are actually "SampleType" on the backend.
    """
    SampleType = 1
    ContainerType = 2
    ItemType = 3
class WorkflowableType(EntityType):
    """
    Generalized EntityType, including ContainerType and ItemType.
    This class is normally not used directly by end-users, but is used
    within the client for resolving references to SampleType, ContainerType,
    and ItemType that were formerly limited to SampleType prior to 3.0.0.
    Since: 3.0.2.
    """
    def _data_by_name(self):
        # Use names b/c sample_types?name=X is a wildcard search for %X%.
        query = "/api/sample_types?names={}&clses={}".format(
            json.dumps([self.ident]), json.dumps([x.name for x in CoreWorkflowableClass])
        )
        res = base.SESSION.get(query).json()
        if len(res) == 0:
            return {}
        # Can't make an entity type the same name as containertype and itemtype.
        # But it _is_ possible, currently, to make a containertype or itemtype with the
        # same name as an entity type.
        if len(res) == 1:
            return res[0]
        # preference order for 3.0.2: SampleType, ItemType, ContainerType.
        # Prefer this order because SampleType is the most common; ItemType is allowed for
        # sample protocols, but containertype is not.
        # Or else we can raise an exception... should be a rare case. However, raising ->
        # You can't import, which means you can't correct; this way, you can fix it after.
        # But _do_ warn...
        res = sorted(res, key=lambda x: CoreWorkflowableClass[x["cls"]].value)
        warnings.warn(DuplicateWorkflowableTypeNames(self.ident, res, res[0]))
        return res[0]
[docs]class WorkflowableClass(BaseModel):
    """
    Object for interacting with WorkflowableClass from the ESP database.
    See the `Usage <./usage.html>`_ and `Examples <./examples.html>`_ pages
    of the documentation for more context and comprehensive examples of
    how to create and use this type of objects.
    Configuration:
        Simple workflowable type:
        .. code-block:: yaml
            name: Patient
            desc: A Human Patient
            tags: [patient, demo]
            plural_name: Patients
            view_template: <h1>My View Template</h1>
            list_view_template: <h1>My View Template</h1>
            detail_panel_template: <h1>My View Template</h1>
            icon_svg: <svg xmlns=“http://www.w3.org/2000/svg” width=“24" height=“24” viewBox=“0 0 24 24"><g fill=“none” fill-rule=“evenodd”><path d=“M0 0h24v24H0z”/><path fill=“#550185" fill-rule=“nonzero” d=“M6 6v14h12V6h-3v1H9V6H6zm4-1a2 2 0 1 1 4 0h5v16H5V5h5zm1 5h5v1h-5v-1zm0 3h5v1h-5v-1zm-3-3h2v1H8v-1zm0 3h2v1H8v-1zm0 3h2v1H8v-1zm3 0h5v1h-5v-1z”/>
    Examples:
        .. code-block:: python
            >>> from esp.models import WorkflowableClass
            >>> wf = WorkflowableClass('Patient')
            >>> wf.name, wf.created_at
            ('Library', '2019-06-21T16:04:01.199076Z')
    Args:
        ident (str): Name or uuid for object.
    """
    __api__ = "workflowable_resource_classes"
    __api_cls__ = "WorkflowableResourceClass"
    __mutable__ = BaseModel.__mutable__ + [
        "plural_name",
        "view_template",
        "list_view_template",
        "detail_panel_template",
        "icon_svg",
    ]
    __exportable__ = [
        "name",
        "tags",
        "desc",
        "plural_name",
        "view_template",
        "list_view_template",
        "detail_panel_template",
        "icon_svg",
    ]
    __type_aliases__ = ["EntityClass"]
[docs]    @classmethod
    def parse_import(cls, config, overwrite=False, allow_snapshot_uuid_remap=False):
        """
        Create new object in ESP database using config file or other data.
        Args:
            config (str, dict, list): Config file or information to use in
                creating new object.
            overwrite (bool): Whether or not to delete current entry in
                the ESP database.
        """
        # parse view_template contents
        compiled = config.pop("compiled", False)
        if config.get("view_template") is not None:
            config["view_template"] = compile_report(config["view_template"], compiled)
        # parse list_view_template contents
        if config.get("list_view_template") is not None:
            config["list_view_template"] = compile_report(config["list_view_template"], compiled)
        if config.get("detail_panel_template") is not None:
            config["detail_panel_template"] = compile_report(config["detail_panel_template"], compiled)
        # parse icon
        if "icon_svg" in config:
            config["icon_svg"] = espfile_or_value(config["icon_svg"], True)
        return config 
    @classmethod
    def parse_import_hub(cls, config):
        # Pop off 'icon_svg' until we can find a reasonable way to handle it
        # that's not ESP File
        icon_svg = None
        if "icon_svg" in config:
            icon_svg = config.pop("icon_svg")
        config = cls.parse_import(config)
        config["icon_svg"] = icon_svg
        config.pop("_format")
        return config
[docs]    def drop(self, *args, **kwargs):
        if self.name.lower() in ["sample", "container", "item", "mesproduct"]:
            return
        return super(WorkflowableClass, self).drop(*args, **kwargs)  
EntityClass = WorkflowableClass
class Entity(LinkedModel):
    """
    Object for interacting with Entities from the ESP database.
    See the `Usage <./usage.html>`_ and `Examples <./examples.html>`_ pages
    of the documentation for more context and comprehensive examples of
    how to create and use this type of objects.
    .. note:: Previous versions of ESP used the word ``Sample`` instead
              of ``Entity``. The python client currently uses the old
              ``Sample`` nomenclature for ``Entity`` objects in the system,
              which will be deprecated in favor of ``Entity`` in future versions.
    Configuration:
        Simple entity:
        .. code-block:: yaml
            name:  LIB0001
        Entity of specific type with note and tags:
        .. code-block:: yaml
            name:  LIB0001
            desc: Library sample
            tags: [library, special-sample-1]
            type: Library
        Entity with variable defaults:
        .. code-block:: yaml
            name:  LIB0001
            type: Library
            variables:
                Entity Type: Illumina Library
                Numeric Value: 10
        Entity batch:
        .. code-block:: yaml
            count: 10
            type: Illumina Library
        Entity batch with variable defaults:
        .. code-block:: yaml
            count: 10
            type: Illumina Library
            variables:
                Entity Type: Illumina Library
                Numeric Value: 10
    Configuration Notes:
        * Either single entities or batches of entities can be created with
          the Entity model ``create`` method.
        * Default entity values can be set for entities using the
          ``variables`` parameter.
    Examples:
        .. code-block:: python
            >>> from esp.models import Entity
            >>> library = Entity('Library 1')
            >>> library.name, library.created_at
            ('Demo Consumable', '2019-06-21T16:04:01.199076Z')
            >>> # show relationships
            >>> library.projects
            [<Project(name=My Project)>]
            >>> library.experiments
            [<Experiment(name=My Experiment 1)>, <Experiment(name=My Experiment 2)>]
            >>> library.containers
            [<Container(name=Freezer 1)>, <Container(name=Freezer 3)>]
            >>> library.parents
            [<Entity(name=Parent 1)>]
            >>> library.children
            [<Entity(name=Child 1)>, <Entity(name=Child 2)>]
            >>> # data accessors
            >>> library.variables
            {'Entity Type': 'Illumina Library', 'Numeric Value': 2}
            >>> library.locations
            {
              'Freezer 1': 'A01',
              'Freezer 3': 'B02'
            }
            >>> library.metadata
            [{'My Workflow': {'My Protocol': {'Column 1': 'Value 1', 'Column 2': 'Value 2'}}]
            >>> # parent-child relationships
            >>> other = Entity('Other Entity')
            >>> other.add_children(library)
            >>> other.add_children([library, Entity('Other Entity')])
            >>> other.add_parents(library)
            >>> other.add_parents([library, Entity('Other Entity')])
    Args:
        ident (str): Name or uuid for object.
    """
    __api__ = "samples"
    __api_cls__ = "Sample"
    __mutable__ = LinkedModel.__mutable__ + [
        "sample_type_uuid",
        "resource_vals",
    ]
    __create_params__ = ["sample_type"]
    __push_format__ = {"resource_vals": lambda x: x._push_variables(x)}
    __exportable__ = BaseModel.__base_exportable__ + ["barcode", "type", "variables"]
    __export_format__ = {"type": lambda x: x.sample_type.name, "variables": lambda x: x._export_variables(x)}
    __pristine__ = ["variables"]
    _converters = {
        "date": _convert_date_value,
        "resource_link": utils.format_resourcelink_value,
        "attachment": utils.format_attachment_value,
    }
    __type_aliases__ = ["Sample"]
    @classmethod
    def _export_variables(cls, obj):
        result = {}
        for r_val in obj.resource_vals:
            r_val_value = r_val["value"]
            r_val_type = r_val["var_type"]
            r_val_name = r_val["name"]
            exporter = "_export_{}_val".format(r_val_type)
            if r_val_value is not None and exporter in globals():
                result[r_val_name] = globals()[exporter](r_val_value)
            else:
                result[r_val_name] = r_val_value
        return result
    @classmethod
    def _push_variables(cls, obj):
        vals = raw(obj.resource_vals)
        rvars = {x["name"]: x for x in obj.sample_type.resource_vars}
        # TODO: Consolidate/centralize value -> string value conversions.
        if isinstance(vals, list):
            keys = [x["name"] for x in vals]
        elif isinstance(vals, dict):
            # ensure we send in the correct order.
            keys = [x["name"] for x in obj.sample_type.resource_vars]
        else:
            raise ValueError("Unhandled resource_vals format: `{}`. Expected list or dict.".format(vals))
        ret = []
        for k in keys:
            value = obj.variables[k]
            var = rvars.get(k, {"name": k, "var_type": "string"})
            if var["var_type"] in cls._converters:
                value = cls._converters[var["var_type"]](value, var)
            ret.append({"name": k, "value": value})
        obj.data.pop("resource_vals")
        return ret
    @classmethod
    def _get_sample_type(cls, config):
        alternate_type_keys = ["entity_type", "sample_type"]
        tkey = "type"
        for type_key in alternate_type_keys:
            if type_key in config:
                tkey = type_key
                break
        if isinstance(config.get(tkey), BaseModel):
            return config.get(tkey)
        else:
            return SampleType(config.pop(tkey, "Generic sample"))
    @classmethod
    def parse_bulk_import(cls, config, overwrite=False):
        """
        Create new object in ESP database using config file or other data.
        This method should be overwritten by subclasses of LinkedModel for
        model-specific logic.
        Examples:
            .. code-block:: python
                >>> # multiple entities with specified entity type.
                >>> objs = Sample.create(
                >>>     count=3,
                >>>     type='Illumina Sample'
                >>> )
                >>> print([o.uuid for o in objs])
                ['8552afc3-882b-41e3-b0cf-1a4cf3f28d33', '96e63b83-1ddc-4e5b-884c-13fcedf5322d']
        Args:
            config (str, dict, list): Config file or information to use in
                creating new object.
            overwrite (bool): Whether or not to delete current entry in
                the ESP database.
        """
        stype = cls._get_sample_type(config)
        names = config.pop("names", [])
        count = config.pop("count", len(names))
        descs = config.pop("descs", [])
        tags = config.pop("tags", [])
        idseq = config.pop("lab7_id_sequence", stype.sequences[0])
        variables = config.get("variables", {})
        if count == 0:
            raise AssertionError("`count` or `names` must be provided and > 0!")
        if tags and len(tags) != len(names):
            raise ValueError("tags must be the same length as names!")
        # create stubs for samples
        samples = [{} for x in range(count)]
        # set sample type for all samples
        for idx, sample in enumerate(samples):
            sample["sample_type_uuid"] = stype.def_uuid
            sample["lab7_id_sequence"] = idseq
            sample["resource_vals"] = []
            if names:
                sample["name"] = names[idx]
            if descs:
                sample["desc"] = descs[idx]
            for rv in stype.variables:
                key = rv["name"]
                v = rv["default_val"]
                if isinstance(variables, (list, tuple)):
                    v = variables[idx].get(key, v)
                elif isinstance(variables.get(key), (list, tuple)):
                    v = variables[key][idx]
                elif key in variables:
                    v = variables[key]
                sample["resource_vals"].append({"name": key, "value": v})
            if tags:
                sample["tags"] = tags[idx]
        return {
            "autogen_names": not bool(names),
            "lab7_id_sequence": idseq,
            "samples": samples,
        }
    @classmethod
    def samples_from_lims_group(cls, config):
        group_name = config["from"]
        group_type = config.get("type", "experiment")
        type_norm = str(group_type).lower()
        protocol_name = config.get("protocol_name")
        if type_norm == "experiment":
            from .project import Experiment as Group
        elif type_norm == "worksheet":
            from .project import SampleSheet as Group
        else:
            raise ValueError('"type" must be one of experiment or worksheet when using the `from` form of samples')
        group = Group(group_name)
        if not group.exists():
            raise ValueError("No such {}: {}".format(group_type, group_name))
        if protocol_name is None:
            return group.protocols[-1].samples
        protocol = [x for x in group.protocols if x.name == protocol_name]
        if not protocol:
            raise ValueError("No such protocol `{}` in {} `{}`".format(protocol_name, group_type, group_name))
        return protocol[0].samples
    @classmethod
    def parse_import(cls, config, overwrite=False, allow_snapshot_uuid_remap=False):
        """
        Create new object in ESP database using config file or other data.
        This method should be overwritten by subclasses of LinkedModel for
        model-specific logic.
        Examples:
            >>> # create single sample using default sample type
            >>> obj = Sample.create('ESP000001')
            >>> print(obj.uuid)
            8552afc3-882b-41e3-b0cf-1a4cf3f28d33
            >>>
            >>> # multiple samples with specific names
            >>> objs = Sample.create(['ESP0000001', 'ESP0000002'])
            >>> print([o.uuid for o in objs])
            ['8552afc3-882b-41e3-b0cf-1a4cf3f28d33', '96e63b83-1ddc-4e5b-884c-13fcedf5322d']
        Args:
            config (str, dict, list): Config file or information to use in
                creating new object.
            overwrite (bool): Whether or not to delete current entry in
                the ESP database.
        """
        # bulk import
        if "count" in config or "names" in config:
            return cls.parse_bulk_import(config, overwrite=overwrite)
        if "from" in config:
            return cls.samples_from_lims_group(config)
        # reformat string input
        if isinstance(config, str):
            config = {"name": config}
        # insert sample with specific name
        stype = cls._get_sample_type(config)
        config["sample_type"] = stype.name
        config["sample_type_uuid"] = stype.def_uuid
        variables = config.pop("variables", {})
        config["resource_vals"] = []
        for rv in stype.variables:
            rv_name = rv.get("name")
            rv_fixed_id = rv.get("fixed_id")
            rv_uuid = rv.get("uuid")
            default_val = rv["default_val"]
            # try to get value by fixed id first
            # else try to get value by name
            v = variables.get(rv_fixed_id, variables.get(rv_name, default_val))
            if rv["var_type"] in cls._converters and v is not None:
                v = cls._converters[rv["var_type"]](v, rv)
            config["resource_vals"].append({"uuid": rv_uuid, "name": rv_name, "value": v})
        config["meta"] = config.pop("data", {})
        cls._children = config.pop("children", [])
        return config
    @classmethod
    def parse_local_import(cls, data):
        from .__base__ import CACHE
        stype_uuid = cached_uuid(SampleType, data["type"])
        stype_mock = SampleType.from_data(CACHE[stype_uuid])
        data["type"] = stype_mock
        data = cls.parse_import(data)
        data["cls"] = cls.__api_cls__
        return data
    @classmethod
    def parse_response(cls, data, overwrite=False, prompt=False, allow_snapshot_uuid_remap=False):
        """
        Method for parsing request and returning
        :param allow_snapshot_uuid_remap:
        """
        # bulk response
        if "samples" in data:
            return [cls.from_data(x) for x in data["samples"]]
        # single response
        children = cls._children
        del cls._children
        sample = cls.from_data(data)
        if len(children):
            sample.add_children(Sample.create(children, overwrite=False))
        return sample
    @classmethod
    def ingest(cls, filename, pipeline=None, container=None, slot=None, tags=list()):
        """
        Do sample ingest via sample registry.
        """
        import pandas
        from .analysis import Pipeline, Analysis
        # resolve inputs
        if isinstance(filename, (list, tuple)) and isinstance(filename[0], dict):
            filename = pandas.DataFrame(filename)
        if isinstance(filename, pandas.DataFrame):
            from io import StringIO
            df = filename
            filename = "ingest.csv"
            fi = StringIO()
            df.to_csv(fi, sep=",", index=False)
            fh = fi.getvalue().encode("utf-8")
            fi.close()
        elif isinstance(filename, six.string_types):
            with open(filename, "rb") as fi:
                fh = fi.read()
        else:
            raise AssertionError("Error: no rule for ingesting object of type {}".format(str(type(filename))))
        # configure pipeline for ingest
        if pipeline is None:
            pipeline = Pipeline.create(
                name="Generic Ingest",
                desc="Pipeline for performing generic data ingests.",
                tasks=[
                    dict(
                        name="Generic Ingest",
                        desc="Task for performing generic data ingests.",
                        cmd="{{param('apps', 'client')}} ingest --tags='{{tags}}' --container-uuid='{{container_uuid}}' --container-slot='{{container_slot}}' generic {{infile}}",
                    )
                ],
                overwrite=False,
            )
        if not isinstance(pipeline, Pipeline):
            pipeline = Pipeline(pipeline)
        # configure container and slots for ingest
        if container is not None:
            from .container import Container
            container = Container(container)
            if not container.exists():
                raise AssertionError("Error: specified container {} does not exist!".format(container.ident))
            if slot is None:
                slot = container.slots[0]
            container = container.uuid
        # construct and send payload
        payload = {
            "format": "text",
            "has_header": True,
            "pipeline_uuid": pipeline.uuid,
            "container_uuid": container,
            "container_slot": slot,
            "sample_tags": tags,
        }
        files = {
            "metadata": (
                None,
                json.dumps(payload),
            ),
            "data": (
                os.path.basename(filename),
                fh,
            ),
        }
        try:
            ctype = base.SESSION.session.headers.pop("Content-Type", None)
            res = base.SESSION.post("/api/samples", files=files)
        finally:
            if ctype is not None:
                base.SESSION.session.headers["Content-Type"] = ctype
        # wait for ingest pipeline to finish
        data = res.json()
        if "PipelineInstances" not in data:
            raise OSError(
                "Pipeline failed to execute, either because of " "insufficient privileges or some other reason."
            )
        pool = Analysis(data["PipelineInstances"][0])
        pool.join()
        # TODO: search for sample by tag and return those created via ingest
        return pool
    @cached.tag("refresh")
    def variables(self):
        """
        Proxy for weird API schema requiring strange 'meta' field.
        """
        # many sample queries return Sample-compatible data structures
        # except that the resource_vals have already been coalesced into
        # a dict.
        rvals = self.resource_vals
        if isinstance(rvals, dict) or (isinstance(rvals, composite) and rvals.meta_type == "dict"):
            return dict(self.resource_vals)
        return {x["name"]: x["value"] for x in self.resource_vals}
    @cached.tag("refresh")
    def variables_by_id(self):
        """
        # returning a dictionary with key fixed id. If fixed id has no value, it defaults to name as key
        # returns None if key not found
        from esp.models import Sample
        sample = Sample('ESP000009')
        value = sample.variables_by_id['fixed_id_here']
        """
        from esp.utils import FixedIdOrName
        return FixedIdOrName(self.variables, self.entity_type.variables)
    @cached
    def entity_type(self):
        """
        Return associated SampleType object.
        """
        # Note: sample_type_uuid for an entity is _always_ a definition UUID.
        # Using .from_definition drops # of backend hits from 2 (or more) to 1.
        # return EntityType.from_definition(self.data.sample_type_uuid)
        return EntityType.from_definition(self.data.sample_type_uuid)
    @property
    def sample_type(self):
        return self.entity_type
    @cached
    def projects(self):
        """
        Return projects associated with sample.
        """
        res = {}
        for exp in self.experiments:
            if exp.project.name not in res:
                res[exp.project.name] = exp.project
        return list(res.values())
    @cached
    def experiments(self):
        """
        Return Experiment objects associated with sample.
        """
        from .project import Experiment
        deps = self.dependencies["dependents"]
        ret = []
        for cat in deps:
            if cat[0]["cls"] == Experiment.__api_cls__:
                for dep in cat:
                    if not isinstance(dep, six.string_types):
                        ret.append(Experiment(dep["uuid"]))
        return ret
    @cached
    def experiment_chains(self):
        """
        Return all ExperimentChain objects associated with sample.
        """
        from .project import ExperimentChain
        chains = []
        for exp in self.experiments:
            res = base.SESSION.get("/api/workflow_chain_instances", params=dict(workflow_instance=exp.uuid)).json()
            for chain in res:
                chain = ExperimentChain(chain["uuid"], data=chain)
                if chain not in chains:
                    chains.append(chain)
        return chains
    @cached
    def containers(self):
        """
        Return container objects associated with sample.
        .. note:: This could be sped up with a backend accessor, but
                  this implementation will work until there is a need
                  for speeding it up.
        """
        from .container import Container
        res = []
        for container in Container.all():
            for slot in container.items:
                items = slot.item_uuid
                if not isinstance(raw(items), list):
                    items = [items] if items is not None else []
                if self.uuid in items:
                    res.append(container)
                    break
        return res
    @cached
    def locations(self):
        """
        Return locations dictionary showing containers and slots
        associated with sample.
        """
        res = {c.name: [] for c in self.containers}
        for container in self.containers:
            for slot in container.items:
                items = slot.item_uuid
                if not isinstance(raw(items), list):
                    items = [items] if items is not None else []
                if self.uuid in items:
                    if slot.label not in res[container.name]:
                        res[container.name].append(slot.label)
        return res
    @cached
    def metadata(self):
        """
        Return worksheet metadata for each sample.
        """
        res = base.SESSION.get("/api/samples/{}/data".format(self.uuid))
        data = res.json()
        return data.get("results", {})
    def metadata_df(self):
        """
        Return worksheet metadata for the sample as a pandas data frame.
        """
        metadata = self.metadata
        rows = []
        for experiment in metadata:
            for protocol in metadata[experiment]:
                for column in metadata[experiment][protocol]:
                    rows.append(
                        {
                            "Experiment": experiment,
                            "Protocol": protocol,
                            "Column": column,
                            "Value": metadata[experiment][protocol][column],
                        }
                    )
        import pandas as pd
        return pd.DataFrame(rows, columns=["Experiment", "Protocol", "Column", "Value"])
Sample = Entity