コード例 #1
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
コード例 #2
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"]
コード例 #3
0
ファイル: inventory.py プロジェクト: karlbecker/pydent
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
ファイル: test_base_adv.py プロジェクト: karlbecker/pydent
    class Author(ModelBase):
        fields = dict(books=HasMany("Book", ref="book_id", callback="foo"))

        def __init__(self):
            super().__init__(books=None)

        def foo(self, *args):
            return [Book.load_from({"id": 10}, fake_session)]
コード例 #5
0
ファイル: test_base_adv.py プロジェクト: karlbecker/pydent
    class Author(ModelBase):
        fields = dict(books=HasMany("Book", ref="book_id", callback="foo"))

        def __init__(self):
            super().__init__(books=None)

        def foo(self, *args):
            return []
コード例 #6
0
class SampleType(FieldTypeInterface, JSONSaveMixin, ModelBase):
    """A SampleType model."""

    fields = dict(
        samples=HasMany("Sample", "SampleType"),
        field_types=HasMany("FieldType",
                            ref="parent_id",
                            additional_args={"parent_class": "SampleType"}),
        # TODO: operation_type_afts
        # TODO: property_afts
        # TODO: add relationships description
    )

    def field_type(self, name, role=None):
        return super().field_type(name, role)

    @property
    def properties(self):
        props = {}
        for ft in self.field_types:
            if ft.ftype == "sample":
                props[ft.name] = [str(aft) for aft in ft.allowable_field_types]
            else:
                props[ft.name] = ft.ftype
        return props

    def new_sample(self, name, description, project, properties=None):
        if properties is None:
            properties = dict()
        sample = self.session.Sample.new(
            name=name,
            sample_type=self,
            description=description,
            project=project,
            properties=properties,
        )
        return sample

    def _get_update_json(self):
        return self.dump(include=("field_types", ))

    def _get_create_json(self):
        return self.dump(include=("field_types", ))

    def __str__(self):
        return self._to_str("id", "name")
コード例 #7
0
def test_has_many():
    """
    Tests the HasMany relationship. Its expected with a MyModel and
    RefModelName, that the params should return a lambda callable equiavalent
    to:

    .. code-block:: python

        params = lambda: x: {"ref_model_id": x.id}

    """

    class RefModel:
        id = 4
        name = "myname"

    # has_many tries to return lambda x: {"ref_model_id": x.id}
    hasmany = HasMany("ModelName", RefModel.__name__)
    assert hasmany.nested == "ModelName"
    assert hasmany.callback_args[1](RefModel) == {"ref_model_id": 4}

    # has_many tries to return lambda x: {"ref_model_name": x.name}
    hasmany = HasMany("ModelName", RefModel.__name__, attr="name")
    assert hasmany.callback_args[1](RefModel) == {"ref_model_name": "myname"}
コード例 #8
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)
コード例 #9
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
コード例 #10
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]
コード例 #11
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")
コード例 #12
0
class Budget(ModelBase):
    """A Budget model."""

    fields = dict(user_budget_associations=HasMany("UserBudgetAssociation", "Budget"))
コード例 #13
0
class OperationType(FieldTypeInterface, SaveMixin, ModelBase):
    """Represents an OperationType, which is the definition of a protocol in
    Aquarium."""

    fields = dict(
        operations=HasMany("Operation", "OperationType"),
        field_types=HasMany(
            "FieldType",
            ref="parent_id",
            additional_args={"parent_class": "OperationType"},
        ),
        codes=HasManyGeneric("Code"),
        cost_model=HasOneFromMany(
            "Code",
            ref="parent_id",
            additional_args={
                "parent_class": "OperationType",
                "name": "cost_model"
            },
        ),
        documentation=HasOneFromMany(
            "Code",
            ref="parent_id",
            additional_args={
                "parent_class": "OperationType",
                "name": "documentation"
            },
        ),
        precondition=HasOneFromMany(
            "Code",
            ref="parent_id",
            additional_args={
                "parent_class": "OperationType",
                "name": "precondition"
            },
        ),
        protocol=HasOneFromMany(
            "Code",
            ref="parent_id",
            additional_args={
                "parent_class": "OperationType",
                "name": "protocol"
            },
        ),
        test=HasOneFromMany(
            "Code",
            ref="parent_id",
            additional_args={
                "parent_class": "OperationType",
                "name": "test"
            },
        ),
    )

    def code(self, accessor):
        if accessor in [
                "protocol",
                "precondition",
                "documentation",
                "cost_model",
                "test",
        ]:
            return getattr(self, accessor)
        return None

    def instance(self, xpos=0, ypos=0):
        operation = self.session.Operation.new(operation_type_id=self.id,
                                               status="planning",
                                               x=xpos,
                                               y=ypos)
        operation.operation_type = self
        operation.init_field_values()
        return operation

    def field_type(self, name, role):
        if self.field_types:
            fts = filter_list(self.field_types, role=role, name=name)
            if len(fts) > 0:
                return fts[0]

    def sample_type(self):
        sample_types = []
        for field_type in self.field_types:
            for allowable_field_type in field_type.allowable_field_types:
                sample_types.append(allowable_field_type.sample_type)
        return sample_types

    def object_type(self):
        object_types = []
        for field_type in self.field_types:
            for allowable_field_type in field_type.allowable_field_types:
                object_types.append(allowable_field_type.object_type)
        return object_types

    def output(self, name):
        return self.field_type(name, "output")

    def input(self, name):
        return self.field_type(name, "input")

    def save(self):
        """Saves the Operation Type to the Aquarium server.

        Requires this Operation Type to be connected to a session.
        """
        return self.reload(self.session.utils.create_operation_type(self))

    def to_save_json(self):
        op_data = self.dump(
            include={
                "field_types": {
                    "allowable_field_types": {}
                },
                "protocol": {},
                "cost_model": {},
                "documentation": {},
                "precondition": {},
                "test": {},
            })

        # Format 'sample_type' and 'object_type' keys for afts
        for ft_d, ft in zip(op_data["field_types"], self.field_types):
            for aft_d, aft in zip(ft_d["allowable_field_types"],
                                  ft.allowable_field_types):
                aft_d["sample_type"] = {"name": aft.sample_type.name}
                aft_d["object_type"] = {"name": aft.object_type.name}
        return op_data

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

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

    def __str__(self):
        return self._to_str("id", "name", "category")
コード例 #14
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)