コード例 #1
0
class Job(ModelBase):
    """A Job model."""

    fields = dict(
        job_associations=HasMany("JobAssociation", "Job"),
        operations=HasManyThrough("Operation", "JobAssociation"),
        state=JSON(),
    )
    methods = ["status"]

    @property
    def is_complete(self):
        return self.pc == -2

    @property
    def uploads(self):
        http = self.session._aqhttp
        return http.get("krill/uploads?job={}".format(self.id))["uploads"]

    @property
    def start_time(self):
        return self.state[0]["time"]

    @property
    def end_time(self):
        return self.state[-2]["time"]
コード例 #2
0
class User(ModelBase):
    """A User model."""

    fields = dict(
        groups=HasMany("Group", "User"),
        user_budget_associations=HasMany("UserBudgetAssociation", "User"),
        budgets=HasManyThrough("Budget", "UserBudgetAssociation"),
        additional=("name", "id", "login"),
        ignore=("password_digest", "remember_token", "key"),
    )

    def __init__(self):
        pass
コード例 #3
0
def test_has_many_through():
    """Tests the HasManyThrough relationship. This is a little more complicated
    than the other relationships, but its expected that with MyModel and
    ThroughModel that the returned params should be:

    .. code-block:: python

        params = lambda x: {"id": [m.my_model_id for m in x.through_models]}


    For example, if a User has many Budgets through a BudgetAssociation then
    basically the user instance will be passed to the lambda and so the
    relationship will attempt to find models based on the following:

    .. code-block:: python

        # gather budget_ids from user's budget_associations
        params = lambda user: {
            "id": [m.budget_id for m in user.budget_associations]}

    When user.budgets is called, whats really happening is that user instance
    is fullfilling the HasManyThrough relationship by finding the budget_ids
    from the user instance's budget_associations.
    The equivalent code for this is:

    .. code-block:: python

        budget_ids = [m.budget_id for m in user.budget_associations]
        budgets = user.where("Budget", {"id": budget_ids})
    """

    class ThisModel:
        pass

    class ThroughModel:
        pass

    this_model = ThisModel()
    through_model = ThroughModel()
    through_model.my_model_id = 4
    this_model.through_models = [through_model]

    hasmanythrough = HasManyThrough("MyModel", "ThroughModel")
    assert hasmanythrough.nested == "MyModel"

    def expected_fxn(model):
        return {"id": [x.my_model_id for x in model.through_models]}

    fxn = hasmanythrough.callback_args[1]
    assert fxn(this_model) == expected_fxn(this_model)
    assert fxn(this_model) == {"id": [4]}
コード例 #4
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)
コード例 #5
0
ファイル: inventory.py プロジェクト: karlbecker/pydent
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
コード例 #6
0
ファイル: inventory.py プロジェクト: karlbecker/pydent
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]
コード例 #7
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)