sapiopycommons 2025.3.21a458__py3-none-any.whl → 2025.3.25a459__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sapiopycommons might be problematic. Click here for more details.

@@ -4,6 +4,7 @@ from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, AbstractRepo
4
4
  from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
5
5
  from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment, ElnExperimentQueryCriteria
6
6
  from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnExperimentStatus, ElnBaseDataType
7
+ from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
7
8
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
8
9
 
9
10
  from sapiopycommons.customreport.auto_pagers import CustomReportDictAutoPager
@@ -209,16 +210,20 @@ class ExperimentReportUtil:
209
210
  ret_val[record] = exp_id
210
211
  return ret_val
211
212
 
213
+ # CR-47491: Support providing a data type name string to receive PyRecordModels instead of requiring a WrapperType.
212
214
  @staticmethod
213
215
  def map_experiments_to_records_of_type(context: UserIdentifier, exp_ids: list[ExperimentIdentifier],
214
- wrapper_type: type[WrappedType]) -> dict[int, list[WrappedType]]:
216
+ wrapper_type: type[WrappedType] | str) \
217
+ -> dict[int, list[WrappedType] | list[PyRecordModel]]:
215
218
  """
216
219
  Return a dictionary mapping each experiment id to a list of records of the given type that were used in each
217
220
  experiment. If an experiment didn't use any records of the given type then it will be mapped to an empty list.
218
221
 
219
222
  :param context: The current webhook context or a user object to send requests from.
220
223
  :param exp_ids: A list of experiment identifiers.
221
- :param wrapper_type: The record model wrapper to use, corresponds to which data type we will query for.
224
+ :param wrapper_type: The record model wrapper or data type name to use, corresponds to which data type we will
225
+ query for. If a data type name is provided, the returned records will be PyRecordModels instead of
226
+ WrappedRecordModels.
222
227
  :return: A dictionary mapping each experiment id to a list of records of the given type that were used in that
223
228
  experiment.
224
229
  """
@@ -227,7 +232,7 @@ class ExperimentReportUtil:
227
232
 
228
233
  user = AliasUtil.to_sapio_user(context)
229
234
  record_handler = RecordHandler(user)
230
- data_type_name: str = wrapper_type.get_wrapper_data_type_name()
235
+ data_type_name: str = AliasUtil.to_data_type_name(wrapper_type)
231
236
 
232
237
  exp_ids: list[int] = AliasUtil.to_notebook_ids(exp_ids)
233
238
  rows = ExperimentReportUtil.__get_record_experiment_relation_rows(user, data_type_name, exp_ids=exp_ids)
@@ -0,0 +1,7 @@
1
+ # CSP paths:
2
+ PLATE_DESIGNER_PLUGIN: str = "com.velox.gwt.client.plugins.multilayerplating.core.ElnMultiLayerPlatingClientPlugin"
3
+
4
+ # 3D Plate Designer entry tags:
5
+ PLATE_IDS_TAG: str = "MultiLayerPlating_Plate_RecordIdList"
6
+ PLATE_CONFIG_PREFS_TAG: str = "MultiLayerPlating_Entry_PrePlating_Prefs"
7
+ PLATE_LAYER_PREFS_TAG: str = "MultiLayerPlating_Entry_Prefs"
@@ -1,43 +1,64 @@
1
1
  from sapiopylib.rest.utils.Protocols import ElnEntryStep
2
+ from sapiopylib.rest.utils.plates.PlatingUtils import PlateLocation
3
+ from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
2
4
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
3
5
 
6
+ from sapiopycommons.datatype.data_fields import PlateDesignerWellElementFields
4
7
  from sapiopycommons.eln.experiment_handler import ExperimentHandler
5
- from sapiopycommons.general.aliases import SapioRecord, RecordIdentifier, AliasUtil
8
+ from sapiopycommons.eln.experiment_tags import PLATE_IDS_TAG
9
+ from sapiopycommons.general.aliases import RecordIdentifier, AliasUtil, RecordModel, FieldValue
6
10
  from sapiopycommons.general.exceptions import SapioException
7
11
  from sapiopycommons.recordmodel.record_handler import RecordHandler
8
12
 
9
- PLATE_IDS_TAG: str = "MultiLayerPlating_Plate_RecordIdList"
13
+ # Shorthands for longer type names.
14
+ WellElement = PlateDesignerWellElementFields
10
15
 
11
16
 
12
- class PlateDesignerEntry:
17
+ # FR-47486: Change the PlateDesignerEntry class to extend ElnEntryStep instead of containing one.
18
+ class PlateDesignerEntry(ElnEntryStep):
13
19
  """
14
20
  A wrapper for 3D plate designer entries in experiments, providing functions for common actions when dealing with
15
21
  such entries.
16
22
  """
17
- step: ElnEntryStep
18
- __exp_handler: ExperimentHandler
19
- __rec_handler: RecordHandler
20
- __plates: list[SapioRecord] | None
21
- __aliquots: list[SapioRecord] | None
22
- __sources: list[SapioRecord] | None
23
- __designer_elements: list[SapioRecord] | None
24
- __plate_ids: list[int] | None
23
+ _exp_handler: ExperimentHandler
24
+ _rec_handler: RecordHandler
25
+ _plates: list[RecordModel] | None
26
+ _aliquots: list[RecordModel] | None
27
+ _sources: list[RecordModel] | None
28
+ _designer_elements: list[RecordModel] | None
29
+ _designer_elements_by_plate: dict[int, list[RecordModel]] | None
30
+ _plate_ids: list[int] | None
25
31
 
26
32
  def __init__(self, step: ElnEntryStep, exp_handler: ExperimentHandler):
27
33
  """
28
34
  :param step: The ElnEntryStep that is the 3D plate designer entry.
29
35
  :param exp_handler: An ExperimentHandler for the experiment that this entry comes from.
30
36
  """
31
- self.step = step
32
- self.__exp_handler = exp_handler
33
- self.__rec_handler = RecordHandler(exp_handler.context)
34
- self.__plates = None
35
- self.__aliquots = None
36
- self.__sources = None
37
- self.__designer_elements = None
38
- self.__plate_ids = None
37
+ super().__init__(exp_handler.protocol, step.eln_entry)
38
+ self._exp_handler = exp_handler
39
+ self._rec_handler = RecordHandler(exp_handler.user)
40
+ self._plates = None
41
+ self._aliquots = None
42
+ self._sources = None
43
+ self._designer_elements = None
44
+ self._designer_elements_by_plate = None
45
+ self._plate_ids = None
46
+
47
+ @property
48
+ def step(self) -> ElnEntryStep:
49
+ return self
50
+
51
+ def clear_cache(self) -> None:
52
+ """
53
+ Clear the caches for the plates and plate designer well elements in this plate designer entry. This will require
54
+ a new webservice call to get the plates and designer elements the next time they are requested.
55
+ """
56
+ self._plates = None
57
+ self._designer_elements = None
58
+ self._designer_elements_by_plate = None
39
59
 
40
- def get_plates(self, wrapper_type: type[WrappedType]) -> list[WrappedType]:
60
+ # CR-47491: Support not providing a wrapper type to receive PyRecordModels instead of WrappedRecordModels.
61
+ def get_plates(self, wrapper_type: type[WrappedType] | None = None) -> list[WrappedType] | list[PyRecordModel]:
41
62
  """
42
63
  Get the plates that are in the designer entry.
43
64
 
@@ -45,13 +66,16 @@ class PlateDesignerEntry:
45
66
  will be invalidated if a set_plates or add_plates call is made, requiring a new webservice call the next time
46
67
  this function is called.
47
68
 
48
- :param wrapper_type: The record model wrapper to use.
69
+ :param wrapper_type: The record model wrapper to use on the plates. If not provided, the returned records will
70
+ be PyRecordModels instead of WrappedRecordModels.
49
71
  :return: A list of the plates in the designer entry.
50
72
  """
51
- if self.__plates is not None:
52
- return self.__plates
53
- self.__plates = self.__rec_handler.query_models_by_id(wrapper_type, self.__get_plate_ids())
54
- return self.__plates
73
+ if self._plates is not None:
74
+ return self._plates
75
+ if wrapper_type is None:
76
+ wrapper_type = "Plate"
77
+ self._plates = self._rec_handler.query_models_by_id(wrapper_type, self.__get_plate_ids())
78
+ return self._plates
55
79
 
56
80
  def set_plates(self, plates: list[RecordIdentifier]) -> None:
57
81
  """
@@ -76,47 +100,50 @@ class PlateDesignerEntry:
76
100
  record_ids: list[int] = AliasUtil.to_record_ids(plates)
77
101
  self.__set_plate_ids(self.__get_plate_ids() + record_ids)
78
102
 
79
- def get_sources(self, wrapper_type: type[WrappedType]) -> list[WrappedType]:
103
+ def get_sources(self, wrapper_type: type[WrappedType] | str) -> list[WrappedType] | list[PyRecordModel]:
80
104
  """
81
105
  Get the source records that were used to populate the plate designer entry's sample table. This looks for the
82
- entries that the plate designer entry is dependent upon and gets their records if they match the data type name
83
- of the given wrapper.
106
+ entries that the plate designer entry is implicitly dependent upon and gets their records if they match the
107
+ data type name of the given wrapper.
84
108
 
85
109
  Makes a webservice call to retrieve the dependent entries if the experiment handler had not already cached it.
86
110
  Makes another webservice call to get the records from the dependent entry and caches them for future calls.
87
111
 
88
- :param wrapper_type: The record model wrapper to use.
112
+ :param wrapper_type: The record model wrapper or data type name of the records to retrieve. If a data type name
113
+ is provided, then the returned records will be PyRecordModels instead of WrappedRecordModels.
89
114
  :return: A list of the source records that populate the plate designer entry's sample table.
90
115
  """
91
- if self.__sources is not None:
92
- return self.__sources
116
+ if self._sources is not None:
117
+ return self._sources
93
118
 
94
119
  records: list[WrappedType] = []
95
- dependent_ids: list[int] = self.step.eln_entry.dependency_set
96
- for step in self.__exp_handler.get_all_steps(wrapper_type):
120
+ dependent_ids: list[int] = self.step.eln_entry.related_entry_id_set
121
+ for step in self._exp_handler.get_all_steps(wrapper_type):
97
122
  if step.get_id() in dependent_ids:
98
- records.extend(self.__exp_handler.get_step_models(step, wrapper_type))
123
+ records.extend(self._exp_handler.get_step_models(step, wrapper_type))
99
124
 
100
- self.__sources = records
101
- return self.__sources
125
+ self._sources = records
126
+ return self._sources
102
127
 
103
- def get_aliquots(self, wrapper_type: type[WrappedType]) -> list[WrappedType]:
128
+ def get_aliquots(self, wrapper_type: type[WrappedType] | None = None) -> list[WrappedType] | list[PyRecordModel]:
104
129
  """
105
130
  Get the aliquots that were created from this plate designer entry upon its submission.
106
131
 
107
132
  Makes a webservice call to retrieve the aliquots from the plate designer entry and caches them for future calls.
108
133
 
109
- :param wrapper_type: The record model wrapper to use.
134
+ :param wrapper_type: The record model wrapper to use. If not provided, the returned records will be
135
+ PyRecordModels instead of WrappedRecordModels.
110
136
  :return: A list of the aliquots created by the plate designer entry.
111
137
  """
112
- if not self.__exp_handler.step_is_submitted(self.step):
138
+ if not self._exp_handler.step_is_submitted(self.step):
113
139
  raise SapioException("The plate designer entry must be submitted before its aliquots can be retrieved.")
114
- if self.__aliquots is not None:
115
- return self.__aliquots
116
- self.__aliquots = self.__exp_handler.get_step_models(self.step, wrapper_type)
117
- return self.__aliquots
140
+ if self._aliquots is not None:
141
+ return self._aliquots
142
+ self._aliquots = self._exp_handler.get_step_models(self.step, wrapper_type)
143
+ return self._aliquots
118
144
 
119
- def get_plate_designer_well_elements(self, wrapper_type: type[WrappedType]) -> list[WrappedType]:
145
+ def get_plate_designer_well_elements(self, wrapper_type: type[WrappedType] | None = None) \
146
+ -> list[WrappedType] | list[PyRecordModel]:
120
147
  """
121
148
  Get the plate designer well elements for the plates in the plate designer entry. These are the records in the
122
149
  system that determine how wells are displayed on each plate in the entry.
@@ -125,28 +152,101 @@ class PlateDesignerEntry:
125
152
  This cache will be invalidated if a set_plates or add_plates call is made, requiring a new webservice call the
126
153
  next time this function is called.
127
154
 
128
- :param wrapper_type: The record model wrapper to use.
155
+ :param wrapper_type: The record model wrapper to use. If not provided, the returned records will be
156
+ PyRecordModels instead of WrappedRecordModels.
157
+ :return: A list of the plate designer well elements in the designer entry.
158
+ """
159
+ if self._designer_elements is not None:
160
+ return self._designer_elements
161
+ if wrapper_type is None:
162
+ wrapper_type: str = WellElement.DATA_TYPE_NAME
163
+ self._designer_elements = self._rec_handler.query_models(wrapper_type,
164
+ WellElement.PLATE_RECORD_ID__FIELD,
165
+ self.__get_plate_ids())
166
+ if self._designer_elements_by_plate is None:
167
+ self._designer_elements_by_plate = {}
168
+ self._designer_elements_by_plate.clear()
169
+ for element in self._designer_elements:
170
+ plate_id: int = element.get(WellElement.PLATE_RECORD_ID__FIELD)
171
+ self._designer_elements_by_plate.setdefault(plate_id, []).append(element)
172
+ return self._designer_elements
173
+
174
+ def get_well_elements_by_plate(self, plate: RecordIdentifier, wrapper_type: type[WrappedType] | None = None) \
175
+ -> list[WrappedType] | list[PyRecordModel]:
176
+ """
177
+ Get the plate designer well elements for the given plate in the plate designer entry. These are the records in
178
+ the system that determine how wells are displayed on the plate in the entry.
179
+
180
+ Makes a webservice call to get the plate designer well elements of the entry and caches them for future calls.
181
+ This cache will be invalidated if a set_plates or add_plates call is made, requiring a new webservice call the
182
+ next time this function is called.
183
+
184
+ :param plate: The plate to get the well elements for.
185
+ :param wrapper_type: The record model wrapper to use. If not provided, the returned records will be
186
+ PyRecordModels instead of WrappedRecordModels.
129
187
  :return: A list of the plate designer well elements in the designer entry.
130
188
  """
131
- if self.__designer_elements is not None:
132
- return self.__designer_elements
133
- self.__designer_elements = self.__rec_handler.query_models(wrapper_type, "PlateRecordId",
134
- self.__get_plate_ids())
135
- return self.__designer_elements
189
+ plate: int = AliasUtil.to_record_id(plate)
190
+ if plate not in self.__get_plate_ids():
191
+ raise SapioException(f"Plate record ID {plate} is not in this plate designer entry.")
192
+ if self._designer_elements_by_plate is None:
193
+ self.get_plate_designer_well_elements(wrapper_type)
194
+ return self._designer_elements_by_plate[plate]
195
+
196
+ def create_well_element(self, sample: RecordModel, plate: RecordModel, location: PlateLocation | None = None,
197
+ wrapper_type: type[WrappedType] | None = None) -> WrappedType | PyRecordModel:
198
+ """
199
+ Create a new plate designer well element for the input sample and plate. A record model manager store and commit
200
+ must be called to save this new well element to the server.
201
+
202
+ :param sample: The sample that the element is for. Must exist in the system (i.e. have a >0 record ID).
203
+ :param plate: The plate that the element is for. Must exist in the system (i.e. have a >0 record ID).
204
+ :param location: The location of the well element. If not provided, the row and column position fields of the
205
+ sample will be used.
206
+ :param wrapper_type: The record model wrapper to use for the plate designer well element. If not provided, the
207
+ returned record will be a PyRecordModel instead of a WrappedRecordModel.
208
+ :return: The newly created PlateDesignerWellElementModel.
209
+ """
210
+ # Confirm that we can actually make a designer element for the input records.
211
+ if AliasUtil.to_record_id(sample) <= 0:
212
+ raise SapioException("Cannot create plate designer well element for sample without a record ID.")
213
+ if AliasUtil.to_record_id(plate) <= 0:
214
+ raise SapioException("Cannot create plate designer well element for plate without a record ID.")
215
+ if AliasUtil.to_data_type_name(sample) != "Sample":
216
+ raise SapioException("Sample record must be of type Sample.")
217
+ if AliasUtil.to_data_type_name(plate) != "Plate":
218
+ raise SapioException("Plate record must be of type Plate.")
219
+
220
+ dt: type[WrappedType] | str = wrapper_type if wrapper_type else WellElement.DATA_TYPE_NAME
221
+ plate_id: int = AliasUtil.to_record_id(plate)
222
+ fields: dict[str, FieldValue] = {
223
+ WellElement.SOURCE_RECORD_ID__FIELD: AliasUtil.to_record_id(sample),
224
+ WellElement.PLATE_RECORD_ID__FIELD: plate_id,
225
+ WellElement.ROW_POSITION__FIELD: location.row_pos if location else sample.get_field_value("RowPosition"),
226
+ WellElement.COL_POSITION__FIELD: str(location.col_pos) if location else sample.get_field_value("ColPosition"),
227
+ WellElement.SOURCE_DATA_TYPE_NAME__FIELD: "Sample",
228
+ }
229
+ element = self._rec_handler.add_models_with_data(dt, [fields])[0]
230
+
231
+ # Add the new element to the cache.
232
+ if self._designer_elements is not None:
233
+ self._designer_elements.append(element)
234
+ if self._designer_elements_by_plate is not None and plate_id in self._designer_elements_by_plate:
235
+ self._designer_elements_by_plate.setdefault(plate_id, []).append(element)
236
+ return element
136
237
 
137
238
  def __get_plate_ids(self) -> list[int]:
138
- if self.__plate_ids is not None:
139
- return self.__plate_ids
140
- id_tag: str = self.__exp_handler.get_step_option(self.step, PLATE_IDS_TAG)
239
+ if self._plate_ids is not None:
240
+ return self._plate_ids
241
+ id_tag: str = self._exp_handler.get_step_option(self.step, PLATE_IDS_TAG)
141
242
  if not id_tag:
142
243
  raise SapioException("No plates in the plate designer entry")
143
- self.__plate_ids = [int(x) for x in id_tag.split(",")]
144
- return self.__plate_ids
244
+ self._plate_ids = [int(x) for x in id_tag.split(",")]
245
+ return self._plate_ids
145
246
 
146
247
  def __set_plate_ids(self, record_ids: list[int]) -> None:
147
248
  record_ids.sort()
148
- self.__exp_handler.add_step_options(self.step, {PLATE_IDS_TAG: ",".join([str(x) for x in record_ids])})
149
- self.__plate_ids = record_ids
249
+ self._exp_handler.add_step_options(self.step, {PLATE_IDS_TAG: ",".join([str(x) for x in record_ids])})
250
+ self._plate_ids = record_ids
150
251
  # The plates and designer elements caches have been invalidated.
151
- self.__plates = None
152
- self.__designer_elements = None
252
+ self.clear_cache()