sapiopycommons 2025.4.10a478__py3-none-any.whl → 2025.4.12a480__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.

@@ -38,6 +38,7 @@ from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, Rec
38
38
  from sapiopycommons.general.custom_report_util import CustomReportUtil
39
39
  from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException, \
40
40
  SapioDialogTimeoutException
41
+ from sapiopycommons.general.time_util import TimeUtil
41
42
  from sapiopycommons.recordmodel.record_handler import RecordHandler
42
43
 
43
44
  DataTypeLayoutIdentifier: TypeAlias = DataTypeLayout | str | None
@@ -682,7 +683,8 @@ class CallbackUtil:
682
683
  default_modifier: FieldModifier | None = None,
683
684
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
684
685
  group_by: FieldIdentifier | None = None,
685
- image_data: Iterable[bytes] | None = None) -> list[FieldMap]:
686
+ image_data: Iterable[bytes] | None = None,
687
+ index_field: str | None = None) -> list[FieldMap]:
686
688
  """
687
689
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
688
690
  a given list of records of a singular type.
@@ -712,6 +714,12 @@ class CallbackUtil:
712
714
  The user may remove this grouping if they want to.
713
715
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
714
716
  the image data list corresponds to the element at the same index in the records list.
717
+ :param index_field: If provided, the returned field maps will contain a field with this name that is equal to
718
+ the record ID of the record at the same index in the records list. This can be used to map the results
719
+ back to the original records. This is used instead of using a RecordId field, as the RecordId field has
720
+ special behavior in the system that can cause issues if the given records are uncommitted record models
721
+ with negative record IDs, meaning we don't want to have a RecordId field in the field maps provided to the
722
+ system.
715
723
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
716
724
  value from the user for that field for each row.
717
725
  """
@@ -723,7 +731,10 @@ class CallbackUtil:
723
731
  if not records:
724
732
  raise SapioException("No records provided.")
725
733
  data_type: str = AliasUtil.to_singular_data_type_name(records)
726
- field_map_list: list[FieldMap] = AliasUtil.to_field_map_list(records)
734
+ if index_field is not None:
735
+ field_map_list: list[FieldMap] = self.__get_indexed_field_maps(records, index_field)
736
+ else:
737
+ field_map_list: list[FieldMap] = AliasUtil.to_field_map_list(records)
727
738
 
728
739
  # Convert the group_by parameter to a field name.
729
740
  if group_by is not None:
@@ -793,13 +804,17 @@ class CallbackUtil:
793
804
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
794
805
  the image data list corresponds to the element at the same index in the records list.
795
806
  """
807
+ # Index the records with a field name that is the current time in milliseconds. This is done to avoid
808
+ # collisions with any existing field names.
809
+ index_field: str = f"_{TimeUtil.now_in_millis()}"
796
810
  results: list[FieldMap] = self.record_table_dialog(title, msg, fields, records,
797
811
  default_modifier=default_modifier,
798
812
  field_modifiers=field_modifiers,
799
- group_by=group_by, image_data=image_data)
813
+ group_by=group_by, image_data=image_data,
814
+ index_field=index_field)
800
815
  records_by_id: dict[int, SapioRecord] = self.rec_handler.map_by_id(records)
801
816
  for result in results:
802
- records_by_id[result["RecordId"]].set_field_values(result)
817
+ records_by_id[result[index_field]].set_field_values(result)
803
818
 
804
819
  def create_record_table_dialog(self,
805
820
  title: str,
@@ -873,7 +888,8 @@ class CallbackUtil:
873
888
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
874
889
  column_positions: dict[str, tuple[int, int]] | None = None,
875
890
  group_by: FieldIdentifier | None = None,
876
- image_data: Iterable[bytes] | None = None) -> list[FieldMap]:
891
+ image_data: Iterable[bytes] | None = None,
892
+ index_field: str | None = None) -> list[FieldMap]:
877
893
  """
878
894
  Create a dialog where the user may input data into the specified fields. The dialog is constructed from
879
895
  a given list of records of a singular type.
@@ -909,6 +925,12 @@ class CallbackUtil:
909
925
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
910
926
  the image data list corresponds to the element at the same index in the records list. Only used if the
911
927
  adaptive dialog becomes a table.
928
+ :param index_field: If provided, the returned field maps will contain a field with this name that is equal to
929
+ the record ID of the record at the same index in the records list. This can be used to map the results
930
+ back to the original records. This is used instead of using a RecordId field, as the RecordId field has
931
+ special behavior in the system that can cause issues if the given records are uncommitted record models
932
+ with negative record IDs, meaning we don't want to have a RecordId field in the field maps provided to the
933
+ system. Only used if the adaptive dialog becomes a table.
912
934
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
913
935
  value from the user for that field for each row. Even if a form was displayed, the field values will still
914
936
  be returned in a list.
@@ -921,7 +943,7 @@ class CallbackUtil:
921
943
  default_modifier=default_modifier, field_modifiers=field_modifiers)]
922
944
  return self.record_table_dialog(title, msg, fields, records,
923
945
  default_modifier=default_modifier, field_modifiers=field_modifiers,
924
- group_by=group_by, image_data=image_data)
946
+ group_by=group_by, image_data=image_data, index_field=index_field)
925
947
 
926
948
  def set_record_adaptive_dialog(self,
927
949
  title: str,
@@ -1685,6 +1707,27 @@ class CallbackUtil:
1685
1707
  with io.BytesIO(FileUtil.zip_files(files)) as data:
1686
1708
  self.callback.send_file(zip_name, False, data)
1687
1709
 
1710
+ @staticmethod
1711
+ def __get_indexed_field_maps(records: Iterable[SapioRecord], index_field: str) -> list[FieldMap]:
1712
+ """
1713
+ For dialogs that accept multiple records, we may want to be able to match the returned results back to the
1714
+ records that they're for. In this case, we need to add an index to each record so that we can match them back
1715
+ to the original records. We can't use the RecordId field, as new record models have negative record IDs that
1716
+ cause the callback dialogs to bug out if the RecordId field is present and negative.
1717
+
1718
+ :param records: The records to return indexed field maps of.
1719
+ :param index_field: The name of the field to use as the index. Make sure that this field doesn't exist on the
1720
+ records, as then it will overwrite the existing value.
1721
+ :return: A list of field maps for the records, with an index field added to each. The value of the index on
1722
+ each field map is the record's record ID (even if it's a record model with a negative ID).
1723
+ """
1724
+ ret_val: list[FieldMap] = []
1725
+ for record in records:
1726
+ field_map: FieldMap = AliasUtil.to_field_map(record)
1727
+ field_map[index_field] = AliasUtil.to_record_id(record)
1728
+ ret_val.append(field_map)
1729
+ return ret_val
1730
+
1688
1731
  @staticmethod
1689
1732
  def __temp_dt_from_field_defs(data_type: DataTypeIdentifier, display_name: str | None,
1690
1733
  plural_display_name: str | None, fields: Iterable[AbstractVeloxFieldDefinition],
@@ -38,12 +38,12 @@ from sapiopylib.rest.utils.plates.PlatingUtils import PlatingOrder
38
38
  class ExperimentStepFactory:
39
39
  user: SapioUser
40
40
  _exp_handler: ExperimentHandler
41
-
41
+
42
42
  _exp_id: int
43
43
 
44
44
  _eln_man: ElnManager
45
45
  _exp_cache: ExperimentCacheManager
46
-
46
+
47
47
  def __init__(self, exp_handler: ExperimentHandler):
48
48
  self.user = exp_handler.user
49
49
  self._exp_handler = exp_handler
@@ -198,7 +198,8 @@ class ExperimentStepFactory:
198
198
  elif isinstance(field_set, str):
199
199
  field_sets.add(self._exp_cache.get_field_set(field_set).field_set_id)
200
200
  entry_criteria.field_set_id_list = list(field_sets)
201
- entry_criteria.field_definition_list = criteria.field_definitions
201
+ if criteria.field_definitions:
202
+ entry_criteria.field_definition_list = list(criteria.field_definitions)
202
203
  if criteria.predefined_field_names:
203
204
  if entry_criteria.field_definition_list is None:
204
205
  entry_criteria.field_definition_list = []
@@ -323,7 +324,8 @@ class ExperimentStepFactory:
323
324
  elif isinstance(field_set, str):
324
325
  field_sets.add(self._exp_cache.get_field_set(field_set).field_set_id)
325
326
  entry_criteria.field_set_id_list = list(field_sets)
326
- entry_criteria.field_definition_list = list(criteria.field_definitions)
327
+ if criteria.field_definitions:
328
+ entry_criteria.field_definition_list = list(criteria.field_definitions)
327
329
  if criteria.predefined_field_names:
328
330
  if entry_criteria.field_definition_list is None:
329
331
  entry_criteria.field_definition_list = []
@@ -167,7 +167,7 @@ class PlateDesignerEntry(ElnEntryStep):
167
167
  self._designer_elements_by_plate = {}
168
168
  self._designer_elements_by_plate.clear()
169
169
  for element in self._designer_elements:
170
- plate_id: int = element.get(WellElement.PLATE_RECORD_ID__FIELD)
170
+ plate_id: int = element.get(WellElement.PLATE_RECORD_ID__FIELD.field_name)
171
171
  self._designer_elements_by_plate.setdefault(plate_id, []).append(element)
172
172
  return self._designer_elements
173
173
 
@@ -194,7 +194,8 @@ class PlateDesignerEntry(ElnEntryStep):
194
194
  return self._designer_elements_by_plate[plate]
195
195
 
196
196
  def create_well_element(self, sample: RecordModel, plate: RecordModel, location: PlateLocation | None = None,
197
- wrapper_type: type[WrappedType] | None = None) -> WrappedType | PyRecordModel:
197
+ layer: int = 1, wrapper_type: type[WrappedType] | None = None) \
198
+ -> WrappedType | PyRecordModel:
198
199
  """
199
200
  Create a new plate designer well element for the input sample and plate. A record model manager store and commit
200
201
  must be called to save this new well element to the server.
@@ -203,6 +204,7 @@ class PlateDesignerEntry(ElnEntryStep):
203
204
  :param plate: The plate that the element is for. Must exist in the system (i.e. have a >0 record ID).
204
205
  :param location: The location of the well element. If not provided, the row and column position fields of the
205
206
  sample will be used.
207
+ :param layer: The layer that the well element is on.
206
208
  :param wrapper_type: The record model wrapper to use for the plate designer well element. If not provided, the
207
209
  returned record will be a PyRecordModel instead of a WrappedRecordModel.
208
210
  :return: The newly created PlateDesignerWellElementModel.
@@ -216,6 +218,8 @@ class PlateDesignerEntry(ElnEntryStep):
216
218
  raise SapioException("Sample record must be of type Sample.")
217
219
  if AliasUtil.to_data_type_name(plate) != "Plate":
218
220
  raise SapioException("Plate record must be of type Plate.")
221
+ if layer < 1:
222
+ raise SapioException("Layer must be greater than 0.")
219
223
 
220
224
  dt: type[WrappedType] | str = wrapper_type if wrapper_type else WellElement.DATA_TYPE_NAME
221
225
  plate_id: int = AliasUtil.to_record_id(plate)
@@ -225,6 +229,7 @@ class PlateDesignerEntry(ElnEntryStep):
225
229
  WellElement.ROW_POSITION__FIELD: location.row_pos if location else sample.get_field_value("RowPosition"),
226
230
  WellElement.COL_POSITION__FIELD: str(location.col_pos) if location else sample.get_field_value("ColPosition"),
227
231
  WellElement.SOURCE_DATA_TYPE_NAME__FIELD: "Sample",
232
+ WellElement.LAYER__FIELD: layer,
228
233
  }
229
234
  element = self._rec_handler.add_models_with_data(dt, [fields])[0]
230
235
 
@@ -59,8 +59,8 @@ class StepCreation:
59
59
  self.is_removable = None
60
60
  self.is_renamable = None
61
61
  self.is_static_view = None
62
- self._related_entry_set = None
63
- self._dependency_set = None
62
+ self.related_entry_set = None
63
+ self.dependency_set = None
64
64
  self.requires_grabber_plugin = False
65
65
  self.entry_singleton_id = None
66
66
  self.is_hidden = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2025.4.10a478
3
+ Version: 2025.4.12a480
4
4
  Summary: Official Sapio Python API Utilities Package
5
5
  Project-URL: Homepage, https://github.com/sapiosciences
6
6
  Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
@@ -1,6 +1,6 @@
1
1
  sapiopycommons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  sapiopycommons/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- sapiopycommons/callbacks/callback_util.py,sha256=rK9wlV3BMZVy9HlR04po5RSnn7T-stePRI6tE8jOPao,131722
3
+ sapiopycommons/callbacks/callback_util.py,sha256=_itd4vQuwsCfucJlWsK_q9KO51qLAs9G4CwpKpvyJS4,134962
4
4
  sapiopycommons/callbacks/field_builder.py,sha256=rnIP-RJafk3mZlAx1eJ8a0eSW9Ps_L6_WadCmusnENw,38772
5
5
  sapiopycommons/chem/IndigoMolecules.py,sha256=slM2y39zZFHc468c366EqR8T-GYJ24UnM9HWAqWFEwQ,3900
6
6
  sapiopycommons/chem/Molecules.py,sha256=5PzRyE1s-Z3nfwh3Y4dCNdQOIJGhog08wyZvgTkKwyU,12384
@@ -18,10 +18,10 @@ sapiopycommons/eln/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
18
18
  sapiopycommons/eln/experiment_cache.py,sha256=kiYZBZEAuQpv-rFviv8_6g7uvXdzH9BMUEprvAHT0Ds,8181
19
19
  sapiopycommons/eln/experiment_handler.py,sha256=7vYRlYAW_xT5LcZ5nLqL9dxEHGRAPEuUxX8d8TElWV8,105742
20
20
  sapiopycommons/eln/experiment_report_util.py,sha256=NNNNPVD3_2ZAjoOqCMOnlnmPD0SCjDcgYi453ATSJBs,37027
21
- sapiopycommons/eln/experiment_step_factory.py,sha256=6mUv2JXN5-MCM_aUblcv95v4NMTp9i1W42AqJ8sdUkM,26007
21
+ sapiopycommons/eln/experiment_step_factory.py,sha256=qw9UfLslVzB6dEIZPOZ85XHKpld81RhD4-csM6TgQNg,26099
22
22
  sapiopycommons/eln/experiment_tags.py,sha256=7-fpOiSqrjbXmWIJhEhaxMgLsVCPAtKqH8xRzpDVKoE,356
23
- sapiopycommons/eln/plate_designer.py,sha256=ix2cflz13PAHyu4deS3d5Qd3kQXk0C7IQxBQ2Dm9fEM,13692
24
- sapiopycommons/eln/step_creation.py,sha256=DiniCKef2Nkpa2QeDiRvh_Q_QBu6J7o7vNlU28TaYRI,10141
23
+ sapiopycommons/eln/plate_designer.py,sha256=8VO3T272jY3egQFN5SiIAQFlbKvWcFUJ_nHGFHrpf6c,13927
24
+ sapiopycommons/eln/step_creation.py,sha256=d_2fEpGDMyas4nx2W_WviFaMsfw95epTT7sJoDuE94g,10139
25
25
  sapiopycommons/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  sapiopycommons/files/complex_data_loader.py,sha256=T39veNhvYl6j_uZjIIJ8Mk5Aa7otR5RB-g8XlAdkksA,1421
27
27
  sapiopycommons/files/file_bridge.py,sha256=vKbqxPexi15epr_-_qLrEfYoxNxB031mXN92iVtOMqE,9511
@@ -61,7 +61,7 @@ sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
61
61
  sapiopycommons/webhook/webhook_context.py,sha256=D793uLsb1691SalaPnBUk3rOSxn_hYLhdvkaIxjNXss,1909
62
62
  sapiopycommons/webhook/webhook_handlers.py,sha256=tUVNCw05CDGu1gFDm2g558hX_O203WVm_n__ojjoRRM,39841
63
63
  sapiopycommons/webhook/webservice_handlers.py,sha256=tyaYGG1-v_JJrJHZ6cy5mGCxX9z1foLw7pM4MDJlFxs,14297
64
- sapiopycommons-2025.4.10a478.dist-info/METADATA,sha256=zR9Q0cvk7jTw7kBfCAGcyrnLidMj35B6s_KmYvcQZiU,3143
65
- sapiopycommons-2025.4.10a478.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
66
- sapiopycommons-2025.4.10a478.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
67
- sapiopycommons-2025.4.10a478.dist-info/RECORD,,
64
+ sapiopycommons-2025.4.12a480.dist-info/METADATA,sha256=8W1_6UTqz6uySW-g2Szb4t1DdOB4-mftKde3svquT3E,3143
65
+ sapiopycommons-2025.4.12a480.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
66
+ sapiopycommons-2025.4.12a480.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
67
+ sapiopycommons-2025.4.12a480.dist-info/RECORD,,