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"]
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
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]}
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)
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
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]
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)