示例#1
0
class UserBudgetAssociation(ModelBase):
    """An association model between a User and a Budget."""

    fields = dict(budget=HasOne("Budget"), user=HasOne("User"))

    def __init__(self):
        pass
示例#2
0
文件: plan.py 项目: karlbecker/pydent
class PlanAssociation(ModelBase):
    """A PlanAssociation model."""

    fields = dict(plan=HasOne("Plan"), operation=HasOne("Operation"))

    def __init__(self, plan_id=None, operation_id=None):
        super().__init__(plan_id=plan_id, operation_id=operation_id)
示例#3
0
class ObjectType(SaveMixin, ModelBase):
    """A ObjectType model that represents the type of container an item is."""

    fields = dict(items=HasMany("Item", "ObjectType"),
                  sample_type=HasOne("SampleType"))

    def __str__(self):
        return self._to_str("id", "name")

    def new_item(self, sample: Union[int, Sample]):
        """Create a new item.

        .. versionadded:: 0.1.5a13

        :param sample: the sample id
        :return: the new item
        """
        if isinstance(sample, int):
            sample_id = sample
            sample = self.session.Sample.find(sample_id)
        elif isinstance(sample, Sample):
            sample_id = sample.id
        else:
            raise TypeError("Sample must be either a sample_id (int) or a"
                            " Sample instance.")
        return self.session.Item.new(sample=sample,
                                     sample_id=sample_id,
                                     object_type=self,
                                     object_type_id=self.id)
示例#4
0
def test_has_one():
    """Tests the HasOne relationship. Its expected that with MyModel, that the
    returned params should be:

    .. code-block:: python

        params = lambda x: x.my_model_id
    """

    class MyModel:
        my_model_id = 4
        my_model_name = "myname"

    # has_many tries to return lambda x: x.my_model_id
    hasone = HasOne("MyModel")
    assert hasone.nested == "MyModel"
    assert hasone.callback_args[1](MyModel) == 4

    # has_many tries to return lambda x: x.my_model_name
    hasone = HasOne("MyModel", attr="name")
    assert hasone.callback_args[1](MyModel) == "myname"
示例#5
0
class PartAssociation(JSONSaveMixin, ModelBase):
    """Represents a PartAssociation linking a part to a collection.

    Collections contain many `parts`, each of which can refer to a
    different sample.
    """

    fields = dict(part=HasOne("Item", ref="part_id"),
                  collection=HasOne("Collection"))

    def __init__(self,
                 part_id=None,
                 collection_id=None,
                 row=None,
                 column=None):
        super().__init__(part_id=part_id,
                         collection_id=collection_id,
                         row=row,
                         column=column)

    def get_sample_id(self) -> Union[None, int]:
        if self.sample_id:
            return self.sample_id
        return self.sample.id

    def has_unsaved_sample(self) -> bool:
        if self.is_deserialized("part") and self.part.is_deserialized(
                "sample"):
            if self.part.sample.id is None:
                return True
        return False

    def is_empty(self) -> bool:
        if self.get_sample_id():
            return True
        return False
示例#6
0
class Code(ModelBase):
    """A Code model."""

    fields = dict(
        user=HasOne("User"),
        operation_type=One("OperationType",
                           callback="get_parent",
                           callback_args=None),
        library=One("Library", callback="get_parent", callback_args=None),
    )

    def get_parent(self, parent_class, *args):
        if parent_class != self.parent_class:
            return None
        return self.session.model_interface(self.parent_class).find(
            self.parent_id)

    def update(self):
        # since they may not always be tied to specific parent
        # controllers
        self.session.utils.update_code(self)
示例#7
0
class DataAssociation(JSONDeleteMixin, JSONSaveMixin, ModelBase):
    """A DataAssociation model."""

    fields = dict(object=JSON(), upload=HasOne("Upload"))

    @property
    def value(self):
        return self.object.get(self.key, None)

    @value.setter
    def value(self, new_value):
        self.object = {self.key: new_value}

    # def save(self):
    #     if self.upload and not self.upload_id:
    #         self.upload.save()
    #         self.upload_id = self.upload_id
    #     super().save()
    #
    #     data_association = self.parent.session.DataAssociation.find(self.id)
    #     if data_association.id not in [da.id for da in self.parent.data_associations]:
    #         self.parent.data_associations.append(data_association)
    #         return data_association
    #     else:
    #         for da in self.parent.data_associations:
    #             if da.id == data_association.id:
    #                 return da

    def save(self):
        if self.parent_id is None:
            raise ValueError(
                "Cannot save DataAssociation. `parent_id` cannot be None")
        if self.parent_class is None:
            raise ValueError(
                "Cannot save DataAssociation. `parent_class` cannot be None")
        super().save(do_reload=True)

    def __str__(self):
        return self._to_str("id", "object")
示例#8
0
class JobAssociation(ModelBase):
    """A JobAssociation model."""

    fields = dict(job=HasOne("Job"), operation=HasOne("Operation"))
示例#9
0
文件: plan.py 项目: karlbecker/pydent
class Wire(DeleteMixin, ModelBase):
    """A Wire model."""

    fields = {
        "source": HasOne("FieldValue", ref="from_id"),
        "destination": HasOne("FieldValue", ref="to_id"),
    }

    WIRABLE_PARENT_CLASSES = ["Operation"]

    def __init__(self, source=None, destination=None):

        self._validate_field_values(source, destination)

        if hasattr(source, "id"):
            from_id = source.id
        else:
            from_id = None

        if hasattr(destination, "id"):
            to_id = destination.id
        else:
            to_id = None

        if (source and not destination) or (destination and not source):
            raise AquariumModelError(
                "Cannot wire. Either source ({}) or destination ({}) is None".
                format(source, destination))

        if source:
            if not source.role:
                raise AquariumModelError(
                    "Cannot wire. FieldValue {} does not have a role".format(
                        source.role))
            elif source.role != "output":
                raise AquariumModelError(
                    "Cannot wire an '{}' FieldValue as a source".format(
                        source.role))

        if destination:
            if not destination.role:
                raise AquariumModelError(
                    "Cannot wire. FieldValue {} does not have a role".format(
                        destination.role))
            elif destination.role != "input":
                raise AquariumModelError(
                    "Cannot wire an '{}' FieldValue as a destination".format(
                        destination.role))

        super().__init__(
            **{
                "source": source,
                "from_id": from_id,
                "destination": destination,
                "to_id": to_id,
            },
            active=True,
        )

    @property
    def identifier(self):

        if not self.source:
            source_id = self.from_id
        else:
            source_id = "r" + str(self.source.rid)

        if not self.destination:
            destination_id = self.to_id
        else:
            destination_id = "r" + str(self.destination.rid)
        return "{}_{}".format(source_id, destination_id)

    @classmethod
    def _validate_field_values(cls, src, dest):
        if src:
            cls._validate_field_value(src, "source")
        if dest:
            cls._validate_field_value(dest, "destination")

        if src and dest and src.rid == dest.rid:
            raise AquariumModelError(
                "Cannot create wire because source and destination are the same "
                "instance.")

    @classmethod
    def _validate_field_value(cls, fv, name):
        if not issubclass(type(fv), FieldValue):
            raise AquariumModelError(
                "Cannot create wire because {} FieldValue is {}.".format(
                    name, fv.__class__.__name__))

        if fv.parent_class not in cls.WIRABLE_PARENT_CLASSES:
            raise AquariumModelError(
                "Cannot create wire because the {} FieldValue is has '{}' "
                "parent class. Only {} parent classes are wirable".format(
                    name, fv.parent_class, cls.WIRABLE_PARENT_CLASSES))

    def validate(self):
        self._validate_field_values(self.source, self.destination)

    def to_save_json(self):
        save_json = {
            "id": self.id,
            "from_id": self.source.id,
            "to_id": self.destination.id,
            "from": {
                "rid": self.source.rid
            },
            "to": {
                "rid": self.destination.rid
            },
            "active": self.active,
        }
        return save_json

    def delete(self):
        """Permanently deletes the wire instance on the Aquarium server.

        :return:
        :rtype:
        """
        return self.session.utils.delete_wire(self)

    def show(self, pre=""):
        """Show the wire nicely."""
        source = self.source
        dest = self.destination

        from_op_type_name = "None"
        from_name = "None"
        if source:
            from_op_type_name = self.source.operation.operation_type.name
            from_name = source.name

        to_op_type_name = "None"
        to_name = "None"
        if dest:
            to_op_type_name = self.destination.operation.operation_type.name
            to_name = dest.name

        print(pre + from_op_type_name + ":" + from_name + " --> " +
              to_op_type_name + ":" + to_name)

    def does_wire_to(self, destination):
        if destination and self.destination:
            return destination.rid == self.destination.rid
        return False

    def does_wire_from(self, source):
        if source and self.source:
            return source.rid == self.source.rid
        return False

    def does_wire(self, source, destination):
        """Checks whether this Wire is a wire between the source and
        destination FieldValues.

        If any of the source or destination FieldValues are None,
        returns False.
        """
        if source and destination and self.source and self.destination:
            return (source.rid == self.source.rid
                    and destination.rid == self.destination.rid)
        return False
示例#10
0
文件: plan.py 项目: karlbecker/pydent
class Plan(DataAssociatorMixin, SaveMixin, DeleteMixin, ModelBase):
    """A Plan model."""

    fields = dict(
        data_associations=HasManyGeneric(
            "DataAssociation", additional_args={"parent_class": "Plan"}),
        plan_associations=HasMany("PlanAssociation", "Plan"),
        operations=HasManyThrough("Operation", "PlanAssociation"),
        wires=Many("Wire", callback="_get_wires_from_server"),
        layout=JSON(),
        status=Raw(default="planning"),
        user=HasOne("User"),
    )
    query_hook = {"include": ["plan_associations", "operations"]}

    def __init__(self, name="MyPlan", status="planning"):
        super().__init__(
            name=name,
            status=status,
            data_associations=None,
            plan_associations=None,
            layout={
                "id": 0,
                "children": [],
                "documentation": "No documentation of this module",
                "height": 60,
                "input": [],
                "output": [],
                "name": "no_name",
                "parent_id": -1,
                "width": 160,
                "wires": [],
            },
        )

    def add_operation(self, operation):
        """Adds an operation to the Plan.

        :param operation: Operation to add
        :type operation: Operation
        :return: None
        :rtype: None
        """
        self.append_to_many("operations", operation)

    def add_operations(self, operations):
        """Adds multiple operations to the Plan.

        :param operations: list of Operations
        :type operations: list
        :return: None
        :rtype: None
        """
        for operation in operations:
            self.add_operation(operation)

    # TODO: this is not functional or not needed
    # def has_operation(self, op):
    #     if op is None:
    #         return False
    #     return self.operations and op.rid in [_op.rid for _op in self.operations]

    def find_wires(self, src, dest):
        """Retrieves the wire between a source and destination FieldValues.

        :param src: source FieldValue
        :type src: FieldValue
        :param dest: destination FieldValue
        :type dest: FieldValue
        :return: array of wires between src and dest FieldValues (determined by rid)
        :rtype: array of wires
        """

        found = []
        for wire in self.wires:
            if src.rid == wire.source.rid and dest.rid == wire.destination.rid:
                found.append(wire)
        return found

    def wire(self, src, dest):
        """Creates a new wire between src and dest FieldValues. Returns the new
        wire if it does not exist in the plan. If the wire already exists and
        error_if_exists is True, then the existing wire is returned. Else an
        exception is raised.

        :param src: source field value (the input of the wire)
        :type src: FieldValue
        :param dest: destination field value (the output of the wire)
        :type dest: FieldValue
        :param error_if_exists: Raise an error if the Wire already exists in the plan.
        :type error_if_exists: boolean
        :return: Newly created wire or existing wire (if exists and
            error_if_exists == False)
        :rtype: Wire
        """

        # TODO: these checks are unnecessary?
        # if not self.has_operation(src.operation):
        #     raise AquariumModelError(
        #         "Cannot wire because the wire's source FieldValue {} does "
        #         "not exist in the Plan because its Operation '{}' is not in the plan".format(
        #             src, src.operation
        #         )
        #     )
        # if not self.has_operation(dest.operation):
        #     raise AquariumModelError(
        #         "Cannot wire because the wire's destination FieldValue {} does not "
        #         "exist in the Plan because its Operation '{}' is not in the plan.".format(
        #             dest, dest.operation
        #         )
        #     )

        wire = Wire(source=src, destination=dest)
        self.append_to_many("wires", wire)
        return wire

    def _collect_wires(self):
        incoming_wires = []
        outgoing_wires = []
        if self.operations:
            for operation in self.operations:
                if operation.field_values:
                    for field_value in operation.field_values:
                        if field_value.outgoing_wires:
                            outgoing_wires += field_value.outgoing_wires
                        if field_value.incoming_wires:
                            incoming_wires += field_value.incoming_wires
        return incoming_wires, outgoing_wires

    def _get_wire_dict(self, wires):
        """Return all wires in the plan grouped by the wire identifier."""
        wire_dict = {}
        for w in wires:
            wire_dict.setdefault(w.identifier, list())
            if w not in wire_dict[w.identifier]:
                wire_dict[w.identifier].append(w)
        return wire_dict

    def _get_wires_from_server(self, *args):
        fvs = []
        if self.operations:
            for op in self.operations:
                fvs += op.field_values

        fv_ids = [fv.id for fv in fvs if fv.id is not None]
        wires_from_server = []
        if fv_ids:
            wires_from_server = self.session.Wire.where({
                "from_id": fv_ids,
                "to_id": fv_ids
            })
        return wires_from_server

    def submit(self, user, budget):
        """Submits the Plan to the Aquarium server.

        :param user: User to submit the Plan
        :type user: User
        :param budget: Budget to use for the Plan
        :type budget: Budget
        :return: JSON
        :rtype: dict
        """
        result = self.session.utils.submit_plan(self, user, budget)
        return result

    def all_data_associations(self):
        das = self.data_associations
        for operation in self.operations:
            das += operation.data_associations
            for field_value in operation.field_values:
                if field_value.item:
                    das += field_value.item.data_associations
        return das

    @classmethod
    def interface(cls, session):
        # get model interface from Base class
        model_interface = super().interface(session)

        # make a special find method for plans, as generic method is too minimal.
        def new_find(model_id):
            return model_interface.get("plans/{}.json".format(model_id))

        # override the old find method
        model_interface.find = new_find

        return model_interface

    def validate(self, raise_error=True):
        """Validates the plan.

        :param raise_error: If True, raises an AquariumModelException. If false,
            returns the error messages.
        :type raise_error: boolean
        :return: list of error messages
        :rtype: array
        """
        errors = []

        field_values = []
        for op in self.operations:
            for fv in op.field_values:
                field_values.append(fv)
        fv_keys = [fv._primary_key for fv in field_values]
        for wire in self.wires:
            for _fvtype in ["source", "destination"]:
                field_value = getattr(wire, _fvtype)
                if field_value._primary_key not in fv_keys:
                    msg = (
                        "The FieldValue of a wire Wire(rid={}).{} is missing from the "
                        "list of FieldValues in the plan. Did you forget to add an "
                        "operation to the plan?".format(
                            wire._primary_key, _fvtype))
                    errors.append(msg)
        if raise_error and errors:
            msg = "\n".join(
                ["(ErrNo {}) - {}".format(i, e) for i, e in enumerate(errors)])
            raise AquariumModelError(
                "Plan {} had the following errors:\n{}".format(self, msg))
        return errors

    def to_save_json(self):
        """Returns the json representation of the plan for saving and creating
        Plans on the Aquarium server.

        :return: JSON
        :rtype: dict
        """
        if not self.operations:
            self.operations = []

        if not self.wires:
            self.wires = []

        self.validate(raise_error=True)

        for op in self.operations:
            op.field_values

        json_data = self.dump(
            include={"operations": {
                "field_values": ["sample", "item"]
            }})

        # remove redundant wires
        wire_dict = {}
        for wire in self.wires:
            wire_data = wire.to_save_json()
            attributes = [
                wire_data["from_id"],
                wire_data["from"]["rid"],
                wire_data["to_id"],
                wire_data["to"]["rid"],
            ]
            wire_hash = "*&".join([str(a) for a in attributes])
            wire_dict[wire_hash] = wire_data
        json_data["wires"] = list(wire_dict.values())

        # validate
        fv_rids = []
        fv_id_to_rids = {}
        for op in json_data["operations"]:
            for fv in op["field_values"]:
                if fv["id"]:
                    fv_id_to_rids[fv["id"]] = fv["rid"]
                fv_rids.append(fv["rid"])

        # fix json rids and ids
        warnings = []
        for wire_data in json_data["wires"]:
            if wire_data["from_id"] in fv_id_to_rids:
                wire_data["from"]["rid"] = fv_id_to_rids[wire_data["from_id"]]
            if wire_data["to_id"] in fv_id_to_rids:
                wire_data["to"]["rid"] = fv_id_to_rids[wire_data["to_id"]]
            if not wire_data["from"]["rid"] in fv_rids:
                warnings.append("FieldValue rid={} is missing.".format(
                    wire_data["from"]["rid"]))
            if not wire_data["to"]["rid"] in fv_rids:
                warnings.append("FieldValue rid={} is missing.".format(
                    wire_data["to"]["rid"]))
        if warnings:
            print(fv_rids)
            warn(",".join(warnings))

        if json_data["layout"] is not None:
            json_data["layout"] = json.loads(json_data["layout"])
        else:
            del json_data["layout"]

        return json_data

    def _get_create_json(self):
        return self.to_save_json()

    def _get_update_json(self):
        return self.to_save_json()

    def _get_create_params(self):
        return {"user_id": self.session.current_user.id}

    def _get_update_params(self):
        return {"user_id": self.session.current_user.id}

    def estimate_cost(self):
        """Estimates the cost of the plan on the Aquarium server. This is
        necessary before plan submission.

        :return: cost
        :rtype: dict
        """
        return self.session.utils.estimate_plan_cost(self)

    def field_values(self):
        raise NotImplementedError()

    def step(self):
        """Steps a plan."""
        return self.session.utils.step_plan(self.id)

    def show(self):
        """Print the plan nicely."""
        print(self.name + " id: " + str(self.id))
        for operation in self.operations:
            operation.show(pre="  ")
        for wire in self.wires:
            wire.show(pre="  ")

    def replan(self):
        """Copies or replans the plan.

        Returns a plan copy
        """
        return self.session.utils.replan(self.id)

    def download_files(self, outdir=None, overwrite=True):
        """Downloads all uploads associated with the plan. Downloads happen
        ansynchrounously.

        :param outdir: output directory for downloaded files
        :param overwrite: whether to overwrite files if they exist
        :return: None
        """
        uploads = []
        for da in self.data_associations:
            if da.upload is not None:
                uploads.append(da.upload)
        return Upload.async_download(uploads, outdir, overwrite)
示例#11
0
class Upload(ModelBase):
    """An Upload model."""

    fields = dict(job=HasOne("Job"))

    def __init__(self, job_id=None, file=None):
        """Create a new upload.

        :param job_id: job id to associate the upload to
        :type job_id: int
        :param file: file to upload
        :type file: file object
        """
        super().__init__(job_id=job_id)
        self.file = file

    query_hook = dict(methods=["size", "name", "job"])

    # def _get_uploads_from_job_id(self, job_id):

    def _get_uploads_from_job(self):
        http = self.session._http
        return http.get("krill/uploads?job={}".format(self.job_id))["uploads"]

    def temp_url(self):
        data = self.session.Upload.where({"id": self.id},
                                         methods=["expiring_url"])[0].raw
        return data["expiring_url"]

    @staticmethod
    def _download_file_from_url(url, outpath):
        """Downloads a file from a url.

        :param url: url of file
        :type url: str
        :param outpath: filepath of out file
        :type outpath: str
        :return: http response
        :rtype: str
        """
        response = requests.get(url, stream=True)
        with open(outpath, "wb") as out_file:
            shutil.copyfileobj(response.raw, out_file)
        return response.raw

    @staticmethod
    @make_async(1)
    def async_download(uploads, outdir=None, overwrite=True):
        """Asynchronously downloads from list of :class:`Upload` models.

        :param uploads: list of Uploads
        :type uploads: list
        :param outdir: path to output directory to save downloaded files
        :type outdir: str
        :param overwrite: if True, will overwrite existing files
        :type overwrite: bool
        :return: list of filepaths
        :rtype: list
        """
        return Upload._download_files(uploads, outdir, overwrite)

    @staticmethod
    def _download_files(uploads, outdir, overwrite):
        """Downloads uploaded file from list of :class:`Upload` models.

        :param uploads: list of Uploads
        :type uploads: list
        :param outdir: path to output directory to save downloaded files (defaults to
            current directory)
        :type outdir: str
        :param overwrite: if True, will overwrite existing files (default: True)
        :type overwrite: bool
        :return: list of filepaths
        :rtype: list
        """
        filepaths = []
        for upload in uploads:
            filepath = upload.download(outdir=outdir, overwrite=overwrite)
            filepaths.append(filepath)
        return filepaths

    def fetch(self,
              outdir: str = None,
              filename: str = None,
              overwrite: bool = True):
        """Alias for `download`

        :param outdir: path of directory of output file (default is current directory)
        :param outfile: filename of output file (defaults to upload_filename)
        :param overwrite: whether to overwrite file if it already exists
        :return: filepath of the downloaded file
        """
        return self.download(outdir=outdir,
                             filename=filename,
                             overwrite=overwrite)

    def download(self,
                 outdir: str = None,
                 filename: str = None,
                 overwrite: bool = True):
        """Downloads the uploaded file to the specified output directory. If no
        output directory is specified, the file will be downloaded to the
        current directory.

        :param outdir: path of directory of output file (default is current directory)
        :param outfile: filename of output file (defaults to upload_filename)
        :param overwrite: whether to overwrite file if it already exists
        :return: filepath of the downloaded file
        """
        if outdir is None:
            outdir = "."
        if filename is None:
            filename = "{}_{}".format(self.id, self.upload_file_name)
        filepath = os.path.join(outdir, filename)
        if not os.path.exists(filepath) or overwrite:
            self._download_file_from_url(self.temp_url(), filepath)
        return filepath

    @property
    def data(self):
        """Return the data associated with the upload."""
        result = requests.get(self.temp_url())
        return result.content

    def create(self):
        """Save the upload to the server."""
        return self.session.utils.create_upload(self)

    def save(self):
        return self.create()
示例#12
0
class Collection(ItemLocationMixin, DataAssociatorMixin, SaveMixin,
                 ControllerMixin, ModelBase):
    """A Collection model, such as a 96-well plate, which contains many
    `parts`, each of which can be associated with a different sample."""

    fields = dict(
        object_type=HasOne("ObjectType"),
        data_associations=HasManyGeneric(
            "DataAssociation", additional_args={"parent_class": "Collection"}),
        part_associations=HasMany("PartAssociation", "Collection"),
        parts=HasManyThrough("Item", "PartAssociation", ref="part_id"),
    )
    query_hook = {"methods": ["dimensions"]}

    # TODO: validate dimensions is returning the same dimensions as that in the object_type
    # TODO: init should establish dimensions

    def __init__(
        self,
        object_type: ObjectType = None,
        location: str = None,
        data_associations: List = None,
        parts: List = None,
        part_associations: List = None,
        **kwargs,
    ):
        """Initialize a new Collection.

        .. versionchanged:: 0.1.5a10
            Advanced indexing added for setting and getting samples and data associations

        **Setting samples using new advanced indexing**

        .. code-block:: python

            object_type = session.ObjectType.one(query='rows > 2 AND columns > 2')
            collection = session.Collection.new(object_type=object_type)

            # assign sample '1' to (0, 0) row=0, column=0
            collection[0, 0] = 1

            # assign sample '2' to (1, 2) row=1, column=2
            collection[1, 2] = 2

            # assign sample '3234' to row 3
            collection[3] = 3234

            # assign sample '444' to column 1
            collection[:, 1] = 444

            # assign sample '6' to the whole collection
            collection[:, :] = 6

            # assign sample using Sample instance
            collection[2, 2] = session.Sample.one()

        **Getting samples using new advanced indexing**

        .. code-block:: python

            # get 2d matrix of sample ids
            print(collection.matrix)  # or collection.sample_id_matrix

            # get 2d matrix of Samples assigned at each location
            print(collection.sample_matrix)

            # get 2d matrix of Parts assigned at each location
            print(collection.part_matrix)

            # get 2d matrix of PartAssociations assigned at each location
            collection.part_associations_matrix

            # get 2d matrix of values of DataAssociations at each location
            collection.data_matrix

            # get 2d matrix of DataAssociations at each location
            collection.data_association_matrix

        **Assigning data to locations**

        To assign data, you can use the advanced indexing on the `data_matrix`

        .. code-block:: python

            collection.data_matrix[0, 0] = {'key': 'value'}

            collection.data_matrix[1] = {'key': 'value2'}

            collection.associate_to('key', 'value3', 3, 3)

        You can delete associations using the following:

        .. code-block:: python

            # delete 3, 3
            collection.delete_association_at('key', 3, 3)

            # delete first three rows at column 3
            collection.delete_association_at('key', slice(None, 3, None), 3)

            # delete all of the 'key' associations
            collection.delete_association_at('key', slice(None, None, None), slice(None, None, None))


        :param object_type:
        :param location:
        :param data_associations:
        :param parts:
        :param part_associations:
        :param kwargs:
        """
        if isinstance(object_type, ObjectType):
            object_type = object_type
            object_type_id = object_type.id
            dims = object_type.rows, object_type.columns
        else:
            object_type_id = None
            dims = None

        super().__init__(
            object_type=object_type,
            object_type_id=object_type_id,
            location=location,
            dimensions=dims,
            data_associations=data_associations,
            part_associations=part_associations,
            parts=parts,
            **kwargs,
        )

    def _empty(self):
        nrows, ncols = self.dimensions
        data = []
        for r in range(nrows):
            data.append([None] * ncols)
        return data

    def __part_association_matrix(self):
        data = self._empty()
        if self.part_associations:
            for assoc in self.part_associations:
                data[assoc.row][assoc.column] = assoc
        return data

    @staticmethod
    def _get_part(assoc):
        if assoc is not None:
            return assoc.part

    @staticmethod
    def _get_sample_id(assoc):
        if assoc is not None:
            if assoc.part:
                if assoc.part.sample_id:
                    return assoc.part.sample_id
                elif assoc.part.sample:
                    return assoc.part.sample.id
                return None

    @staticmethod
    def _get_sample(assoc):
        if assoc is not None:
            if assoc.part:
                return assoc.part.sample

    @staticmethod
    def _get_data_association(assoc):
        if assoc is not None:
            if assoc.part:
                if assoc.data_associations:
                    return {a.key: a for a in assoc.part.data_associations}
                else:
                    return {}

    @staticmethod
    def _get_data_value(assoc):
        if assoc is not None:
            if assoc.part:
                if assoc.part.data_associations:
                    return {
                        a.key: a.value
                        for a in assoc.part.data_associations
                    }
                else:
                    return {}

    @staticmethod
    def _no_setter(x):
        raise ValueError("Setter is not implemented.")

    def _set_sample(
        self,
        data: List[List[PartAssociation]],
        r: int,
        c: int,
        sample: Union[int, Sample],
    ):
        if data[r][c]:
            part = data[r][c].part
        else:
            part = self.session.Item.new(object_type_id=self.session.
                                         ObjectType.find_by_name("__Part").id)
            association = self.session.PartAssociation.new(
                part_id=part.id, collection_id=self.id, row=r, column=c)
            association.part = part
            self.append_to_many("part_associations", association)
        if isinstance(sample, int):
            part.sample_id = sample
            part.reset_field("sample")
        elif isinstance(sample, Sample):
            part.sample = sample
            part.sample_id = sample.id
        elif sample is None:
            part.sample = None
            part.sample_id = None
        else:
            raise ValueError(
                "{} must be a Sample instance or an int".format(sample))

    def _set_key_values(
        self,
        data: List[List[PartAssociation]],
        r: int,
        c: int,
        data_dict: Dict[str, Any],
    ):
        part = None
        if data[r][c]:
            part = data[r][c].part

        if not part:
            raise ValueError(
                "Cannot set data to ({r},{c}) because "
                "there is no Sample assigned to that location".format(r=r,
                                                                      c=c))
        for k, v in data_dict.items():
            part.associate(k, v)

    @property
    def _mapping(self):
        factory = MatrixMappingFactory(self.__part_association_matrix())
        default_setter = self._no_setter
        factory.new("part_association", setter=default_setter, getter=None)
        factory.new("part", setter=default_setter, getter=self._get_part)
        factory.new("sample_id",
                    setter=self._set_sample,
                    getter=self._get_sample_id)
        factory.new("sample", setter=default_setter, getter=self._get_sample)
        factory.new("data",
                    setter=self._set_key_values,
                    getter=self._get_data_value)
        factory.new("data_association",
                    setter=default_setter,
                    getter=self._get_data_association)
        return factory

    @property
    def matrix(self):
        """Returns the matrix of Samples for this Collection.

        (Consider using samples of parts directly.)

        .. versionchanged:: 0.1.5a9
            Refactored using MatrixMapping
        """
        return self.sample_id_matrix

    @property
    def part_matrix(self) -> MatrixMapping[Item]:
        """Return a view of :class:`Item <pydent.models.Item>`

        .. versionadded:: 0.1.5a9

        :return: collection as a view of Items (Parts)
        """
        return self._mapping["part"]

    @property
    def part_association_matrix(self) -> MatrixMapping[PartAssociation]:
        """Return a view of part associations.

        .. versionadded:: 0.1.5a9

        :return: collection as a view of PartAssociations
        """
        return self._mapping["part_association"]

    @property
    def sample_id_matrix(self) -> MatrixMapping[int]:
        """Return a view of sample_ids :class:`Sample<pydent.models.Sample>`

        .. versionadded:: 0.1.5a9

        :return: collection as a view of Sample.ids
        """
        return self._mapping["sample_id"]

    @property
    def sample_matrix(self) -> MatrixMapping[Sample]:
        """Return a view of :class:`Sample<pydent.models.Sample>`

        .. versionadded:: 0.1.5a9

        :return: collection as a view of Samples
        """
        return self._mapping["sample"]

    @property
    def data_matrix(self) -> MatrixMapping[Any]:
        """Return a view of values from the.

        :class:`DataAssociation<pydent.models.DataAssociation>`

        .. versionadded:: 0.1.5a9

        :return: collection as a view of DataAssociation values
        """
        return self._mapping["data"]

    @property
    def data_association_matrix(self) -> MatrixMapping[DataAssociation]:
        """Return a view of.

        :class:`DataAssociation<pydent.models.DataAssociation>`

        .. versionadded:: 0.1.5a9

        :return: collection as a view of DataAssociations
        """
        return self._mapping["data_association"]

    def part(self, row, col) -> Item:
        """Returns the part Item at (row, col) of this Collection (zero-
        based)."""
        return self.part_matrix[row, col]

    def as_item(self):
        """Returns the Item object with the ID of this Collection."""
        return self.session.Item.find(self.id)

    # TODO: implement save and create
    def create(self):
        """Create a new empty collection on the server."""
        result = self.session.utils.model_update("collections",
                                                 self.object_type_id,
                                                 self.dump())
        self.id = result["id"]
        self.created_at = result["created_at"]
        self.updated_at = result["updated_at"]
        self.update()
        return self

    def _validate_for_update(self):
        if not self.id:
            raise ValueError(
                "Cannot update part associations since the Collection "
                "is not saved.")
        for association in self.part_associations:
            if association.has_unsaved_sample():
                raise ValueError(
                    "Cannot update. Collection contains Samples ({r},{c})"
                    " that have not yet been saved.".format(
                        r=association.row, c=association.column))

    def update(self):
        self._validate_for_update()
        self.move(self.location)
        for association in self.part_associations:
            if not association.collection_id:
                association.collection_id = self.id
            association.part.save()
            association.part_id = association.part.id
            association.save()
        self.refresh()

    def assign_sample(self, sample_id: int, pairs: List[Tuple[int, int]]):
        """Assign sample id to the (row, column) pairs for the collection.

        :param sample_id: the sample id to assign
        :param pairs: list of (row, column) tuples
        :return: self
        """
        self.controller_method(
            "assign_sample",
            self.get_tableized_name(),
            self.id,
            data={
                "sample_id": sample_id,
                "pairs": pairs
            },
        )
        self.refresh()
        return self

    def remove_sample(self, pairs: List[Tuple[int, int]]):
        """Clear the sample_id assigment in the (row, column) pairs for the
        collection.

        :param pairs: list of (row, column) tuples
        :return: self
        """
        self.controller_method(
            "delete_selection",
            self.get_tableized_name(),
            self.id,
            data={"pairs": pairs},
        )
        self.refresh()
        return self

    def associate_to(self, key, value, r: int, c: int):
        self.data_matrix[r, c] = {key: value}

    def delete_association_at(self, key, r: int, c: int):
        part_matrix = self.part_matrix
        for r, c in part_matrix._iter_indices((r, c)):
            part = part_matrix[r, c]
            part.delete_data_associations(key)

    def __getitem__(
            self, index: IndexType) -> Union[int, List[int], List[List[int]]]:
        """Returns the sample_id of the part at the provided index.

        :param index: either an int (for rows), tuple of ints/slice objs, (row, column),
            or a slice object.
        :return: sample_ids
        """
        return self.matrix[index]

    def __setitem__(self, index: IndexType, sample: Union[int, Sample]):
        """Sets the sample_ids of the collection, creating the PartAssociations
        and parts if needed.

        :param index: either an int (for rows), tuple of ints/slice objs, (row, column),
            or a slice object.
        :param sample: either a Sample with a valid id or sample_id
        :return:
        """
        self.sample_id_matrix[index] = sample
示例#13
0
class Item(DataAssociatorMixin, ItemLocationMixin, ModelBase):
    """A physical object in the lab, which a location and unique id."""

    DID_ITEM_WARNING = False

    fields = dict(
        sample=HasOne("Sample"),
        object_type=HasOne("ObjectType"),
        data_associations=HasManyGeneric(
            "DataAssociation", additional_args={"parent_class": "Item"}),
        data=Raw(),
        ignore=("locator_id", ),
        part_associations=HasMany("PartAssociation",
                                  ref="part_id"),  # TODO: add to change log
        collections=HasManyThrough(
            "Collection", "PartAssociation"),  # TODO: add to change log
    )
    query_hook = {"methods": ["is_part"]}

    def __init__(
        self=None,
        sample_id=None,
        sample=None,
        object_type=None,
        object_type_id=None,
        location=None,
    ):
        if sample_id is None:
            if sample and sample.id:
                sample_id = sample.id

        if object_type_id is None:
            if object_type and object_type.id:
                object_type_id = object_type.id
        super().__init__(
            object_type_id=object_type_id,
            object_type=object_type,
            sample_id=sample_id,
            sample=sample,
            location=location,
        )

    def create(self):
        with DataAssociationSaveContext(self):
            result = self.session.utils.create_items([self])
            self.reload(result[0]["item"])
        return self

    @property
    def containing_collection(self):
        """Returns the collection of which this Item is a part.

        Returns the collection object if the Item is a part, otherwise
        returns None.
        """
        if not self.is_part:
            return None

        assoc_list = self.session.PartAssociation.where({"part_id": self.id})
        if not assoc_list:
            return

        if len(assoc_list) != 1:
            return None

        part_assoc = next(iter(assoc_list))
        if not part_assoc:
            return None

        return self.session.Collection.find(part_assoc.collection_id)

    def as_collection(self):
        """Returns the Collection object with the ID of this Item, which must
        be a collection.

        Returns None if this Item is not a collection.
        """
        if not self.is_collection:
            return None

        return self.session.Collection.find(self.id)

    @property
    def is_collection(self):
        """Returns True if this Item is a collection in a PartAssociation.

        Note: this is not how Aquarium does this test in the `collection?` method.
        """
        assoc_list = self.session.PartAssociation.where(
            {"collection_id": self.id})
        return bool(assoc_list)

    # TODO: add to change log
    @property
    def collection(self):
        return self.collections[0]

    # TODO: add to change log
    @property
    def part_association(self):
        return self.part_associations[0]
示例#14
0
class Sample(FieldValueInterface, ModelBase):
    """A Sample model."""

    fields = dict(
        # sample relationships
        sample_type=HasOne("SampleType"),
        items=HasMany("Item", ref="sample_id"),
        field_values=HasMany("FieldValue",
                             ref="parent_id",
                             additional_args={"parent_class": "Sample"}),
        user=HasOne("User"),
    )

    METATYPE = "sample_type"

    def __init__(
        self,
        name=None,
        project=None,
        description=None,
        sample_type=None,
        sample_type_id=None,
        properties=None,
        field_values=None,
    ):
        """

        :param name:
        :type name:
        :param project:
        :type project:
        :param description:
        :type description:
        :param sample_type_id:
        :type sample_type_id:
        :param properties:
        :type properties:
        """
        super().__init__(
            name=name,
            project=project,
            description=description,
            sample_type_id=sample_type_id,
            sample_type=sample_type,
            field_values=field_values,
            items=None,
        )

        if properties is not None:
            self.update_properties(properties)

    @property
    def identifier(self):
        """Return the identifier used by Aquarium in autocompletes."""
        return "{}: {}".format(self.id, self.name)

    def field_value(self, name):
        """Returns the :class:`FieldValue` associated with the sample by its
        name. If the there is more than one FieldValue with the same name (as
        in field_value arrays), it will return the first FieldValue. See the.

        :meth:`Sample.field_value_array` method.

        :param name: name of the field value
        :type name: str
        :return: the field value
        :rtype: FieldValue
        """
        return self.get_field_value(name, None)

    def field_value_array(self, name):
        return self.get_field_value_array(name, None)

    def _property_accessor(self, fv):
        ft = self.safe_get_field_type(fv)
        if ft:
            if ft.ftype == "sample":
                return fv.sample
            else:
                if ft.ftype == "number":
                    if isinstance(fv.value, str):
                        return json.loads(fv.value)
                return fv.value

    @property
    def properties(self):
        return self._field_value_dictionary(lambda ft: ft.name,
                                            self._property_accessor)

    def field_value_dictionary(self):
        return self._field_value_dictionary(lambda ft: ft.name, lambda x: x)

    def update_properties(self, prop_dict):
        """Update the FieldValues properties for this sample.

        :param prop_dict: values to update
        :type pro fp_dict: dict
        :return: self
        :rtype: Sample
        """
        ft_dict = {ft.name: ft for ft in self.get_field_types()}
        for name, val in prop_dict.items():
            ft = ft_dict[name]
            if ft.is_parameter():
                key = "value"
            else:
                key = "sample"
            if issubclass(type(val), Sequence) and ft.array:
                self.set_field_value_array(name, None, [{key: v} for v in val])
            else:
                self.set_field_value(name, None, {key: val})

    def create(self) -> List["Sample"]:
        """Creates this sample in the Aquarium instance if the sample has all
        fields required by the sample type.
        Uses is_savable to check for missing fields.

        .. versionchanged:: 0.1.5a17
            Raises `AquariumModelError` if sample is missing required properties

        :return: the list of saved samples
        :raises AquariumModelError: if required FieldValues are missing.
        """
        self.is_savable(do_raise=True)
        return self.session.utils.create_samples([self])

    def is_savable(self, do_raise: bool = True) -> Tuple[bool, List[str]]:
        """Checks whether this sample has fields required by the sample type.
           If so, the sample can be saved or updated.

        .. versionadded:: 0.1.5a17

        :param do_raise: raise an exception when required field values are missing
        :return: a boolean whether there are errors, and list of error messages
        :raises AquariumModelError: if do_raise is True and required fields are missing
        """
        errors = []
        for k, v in self.properties.items():
            if v is None and self.sample_type.field_type(k).required:
                errors.append("FieldValue '{}' is required.".format(k))
        if do_raise and errors:
            raise AquariumModelError(
                "Cannot update/save due to the following:\n"
                "Sample: id={} name={} ({})\n\t{}".format(
                    self.id, self.name, self.sample_type.name,
                    "\n\t".join(errors)))
        return len(errors) == 0, errors

    def save(self):
        """Saves the sample, either by creating a new sample (if id=None) or
        updating the existing sample on the server.

        .. versionchanged:: 0.1.5a17
            Raises `AquariumModelError` if sample is missing required properties
        :return:
        """
        if self.id:
            self.update()
        else:
            self.create()

    def update(self) -> "Sample":
        """Updates the sample on the server.

        .. versionchanged:: 0.1.5a17
            Raises `AquariumModelError` if sample is missing required properties
        :return:
        """
        self.is_savable(do_raise=True)
        for fv in self.field_values:
            fv.reload(fv.save())

        new_fvs = self.field_values
        server_fvs = self.session.FieldValue.where(
            dict(parent_id=self.id, parent_class="Sample"))

        to_remove = [
            fv for fv in server_fvs
            if fv.id not in [_fv.id for _fv in new_fvs]
        ]
        if to_remove:
            warn(
                "Trident tried to save a Sample, but it required FieldValues to be deleted."
            )
            for fv in to_remove:
                fv.parent_id = None
                fv.save()
        self.reload(self.session.utils.json_save("Sample", self.dump()))
        return self

    def merge(self):
        """Merge sample by name. If a sample with the same name and
        sample_type_id is found on the server, update that model and save the
        updated data to the server. Else, create a new sample on the server.

        :param sample: sample to merge.
        :return: True if merged, False otherwise
        """
        try:
            self.save()
        except Exception as e:
            existing = self.session.Sample.find_by_name(self.name)
            if existing:
                if self.sample_type_id == existing.sample_type_id:
                    existing.update_properties(self.properties)
                    existing.description = self.description
                    existing.project = self.project
                    existing.save()
                    self.reload(existing.dump())
                    return True
                else:
                    raise e
            else:
                raise e
        return False

    def available_items(self, object_type_name=None, object_type_id=None):
        query = {"name": object_type_name, "id": object_type_id}
        query = {k: v for k, v in query.items() if v is not None}
        if query == {}:
            return [i for i in self.items if i.location != "deleted"]
        else:
            object_types = self.session.ObjectType.where(query)
            object_type = object_types[0]
            return [
                i for i in self.items if i.location != "deleted"
                and i.object_type_id == object_type.id
            ]

    def copy(self):
        """Return a copy of this sample."""
        copied = super().copy()
        copied.anonymize()
        return copied

    def __str__(self):
        return self._to_str("id", "name", "sample_type")
示例#15
0
class Membership(ModelBase):
    fields = dict(user=HasOne("User"), group=HasOne("Group"))
示例#16
0
class Operation(FieldValueInterface, DataAssociatorMixin, ModelBase):
    """A Operation model."""

    fields = dict(
        field_values=HasMany("FieldValue",
                             ref="parent_id",
                             additional_args={"parent_class": "Operation"}),
        data_associations=HasManyGeneric(
            "DataAssociation", additional_args={"parent_class": "Operation"}),
        operation_type=HasOne("OperationType"),
        job_associations=HasMany("JobAssociation", "Operation"),
        jobs=HasManyThrough("Job", "JobAssociation"),
        plan_associations=HasMany("PlanAssociation", "Operation"),
        plans=HasManyThrough("Plan", "PlanAssociation"),
        status=Raw(default="planning"),
        routing=Function("get_routing"),
        user=HasOne("User"),
    )

    METATYPE = "operation_type"

    def __init__(self,
                 operation_type_id=None,
                 operation_type=None,
                 status=None,
                 x=0,
                 y=0):
        super().__init__(
            operation_type_id=operation_type_id,
            operation_type=operation_type,
            status=status,
            field_values=None,
            x=x,
            y=y,
        )

    def get_routing(self):
        routing_dict = {}
        fvs = self.field_values
        ot = self.operation_type
        if ot is None:
            return routing_dict
        for ft in ot.field_types:
            if ft.routing is not None:
                routing_dict[ft.routing] = None
        if fvs is not None:
            for fv in self.field_values:
                ft = self.safe_get_field_type(fv)
                if ft.routing is not None:
                    routing_dict[ft.routing] = fv.sid
        return routing_dict

    @property
    def successors(self):
        successors = []
        if self.outputs:
            for output in self.outputs:
                for s in output.successors:
                    successors.append(s.operation)
        return successors

    @property
    def predecessors(self):
        predecessors = []
        if self.inputs:
            for inputs in self.inputs:
                for s in inputs.predecessors:
                    predecessors.append(s.operation)
        return predecessors

    def init_field_values(self):
        """Initialize the :class:`FieldValue` from the :class:`FieldType` of
        the parent :class:`Operation` type."""
        self.field_values = []
        for field_type in self.get_metatype().field_types:
            if not field_type.array:
                self.new_field_value_from_field_type(field_type)
        return self

    def field_value_array(self, name, role):
        """Returns :class:`FieldValue` array with name and role."""
        return filter_list(self.get_field_value_array(name, role))

    def field_value(self, name, role):
        """Returns :class:`FieldValue` with name and role.

        Return None if not found.
        """
        if self.field_values:
            fvs = self.field_value_array(name, role)

            if len(fvs) == 0:
                return None

            if len(fvs) == 1:
                return fvs[0]

            msg = "More than one FieldValue found for the field value"
            msg += (
                " of operation {}.(id={}).{}.{}. Are you sure you didn't mean to "
                "call 'field_value_array'?")
            raise AquariumModelError(
                msg.format(self.operation_type, self.id, role, name))

    @property
    def plan(self):
        return self.plans[0]

    def input_array(self, name):
        return self.get_field_value_array(name, "input")

    def output_array(self, name):
        return self.get_field_value_array(name, "output")

    def input(self, name):
        """Returns the input :class:`FieldValue` by name."""
        return self.field_value(name, "input")

    def output(self, name):
        """Returns the output :class:`FieldValue` by name."""
        return self.field_value(name, "output")

    def add_to_input_array(self,
                           name,
                           sample=None,
                           item=None,
                           value=None,
                           container=None):
        """Creates and adds a new input :class:`FieldValue`. When setting
        values to items/samples/containers, the item/sample/container must be
        saved.

        :param name: name of the FieldType/FieldValue
        :type name: string
        :param sample: an existing Sample
        :type sample: Sample
        :param item: an existing Item
        :type item: Item
        :param value: a string or number value
        :type value: string|integer
        :param container: an existing ObjectType
        :type container: ObjectType
        :return: the newly created FieldValue
        :rtype: FieldValue
        """
        return self.new_field_value(
            name,
            "input",
            dict(sample=sample, item=item, value=value, container=container),
        )

    def add_to_output_array(self,
                            name,
                            sample=None,
                            item=None,
                            value=None,
                            container=None):
        """Creates and adds a new output :class:`FieldValue`. When setting
        values to items/samples/containers, the item/sample/container must be
        saved.

        :param name: name of the FieldType/FieldValue
        :type name: string
        :param sample: an existing Sample
        :type sample: Sample
        :param item: an existing Item
        :type item: Item
        :param value: a string or number value
        :type value: string|integer
        :param container: an existing ObjectType
        :type container: ObjectType
        :return: the newly created FieldValue
        :rtype: FieldValue
        """
        return self.new_field_value(
            name,
            "output",
            dict(sample=sample, item=item, value=value, container=container),
        )

    @property
    def inputs(self):
        """Return a list of all input :class:`FieldValues`"""
        return [fv for fv in self.field_values if fv.role == "input"]

    @property
    def outputs(self):
        """Return a list of all output :class:`FieldValues`"""
        return [fv for fv in self.field_values if fv.role == "output"]

    def set_input(self,
                  name,
                  sample=None,
                  item=None,
                  value=None,
                  container=None,
                  object_type=None):
        """Sets a input :class:`FieldValue` to a value. When setting values to
        items/samples/containers, the item/sample/container must be saved.

        :param name: name of the FieldValue/FieldType
        :type name: string
        :param sample: an existing Sample
        :type sample: Sample
        :param item: an existing Item
        :type item: Item
        :param value: a string or number value
        :type value: string|integer
        :param container: an existing ObjectType
        :type container: ObjectType
        :return: the existing FieldValue modified
        :rtype: FieldValue
        """
        if object_type is None and container:
            object_type = container
        return self.set_field_value(
            name,
            "input",
            dict(sample=sample, item=item, value=value, object_type=container),
        )

    def set_output(self,
                   name,
                   sample=None,
                   item=None,
                   value=None,
                   container=None,
                   object_type=None):
        """Sets a output :class:`FieldValue` to a value. When setting values to
        items/samples/containers, the item/sample/container must be saved.

        :param name: name of the FieldValue/FieldType
        :type name: string
        :param sample: an existing Sample
        :type sample: Sample
        :param item: an existing Item
        :type item: Item
        :param value: a string or number value
        :type value: string|integer
        :param container: an existing ObjectType
        :type container: ObjectType
        :return: the existing FieldValue modified
        :rtype: FieldValue
        """
        if object_type is None and container:
            object_type = container
        return self.set_field_value(
            name,
            "output",
            dict(sample=sample,
                 item=item,
                 value=value,
                 object_type=object_type),
        )

    def set_input_array(self, name, values):
        """Sets input :class:`FieldValue` array using values. Values should be
        a list of dictionaries containing sample, item, container, or values
        keys. When setting values to items/samples/containers, the
        item/sample/container must be saved.

        :param name: name of the FieldType/FieldValues being modified
        :type name: string
        :param values: list of dictionary of values to set
                (e.g. [{"sample": mysample}, {"item": myitem}])
        :type values: list
        :return: the list of modified FieldValues
        :rtype: list
        """
        return self.set_field_value_array(name, "input", values)

    def set_output_array(self, name, values):
        """Sets output :class:`FieldValue` array using values. Values should be
        a list of dictionaries containing sample, item, container, or values
        keys. When setting values to items/samples/containers, the
        item/sample/container must be saved.

        :param name: name of the FieldType/FieldValues being modified
        :type name: string
        :param values: list of dictionary of values to set
                (e.g. [{"sample": mysample}, {"item": myitem}])
        :type values: list
        :return: the list of modified FieldValues
        :rtype: list
        """
        return self.set_field_value_array(name, "output", values)

    def show(self, pre=""):
        """Print the operation nicely."""
        print(pre + self.operation_type.name + " " + str(self.cost))
        for field_value in self.field_values:
            field_value.show(pre=pre + "  ")

    def __str__(self):
        return self._to_str(operation_type_name=self.operation_type.name)