sapiopycommons 2025.6.11a561__py3-none-any.whl → 2025.6.18a563__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.

Files changed (54) hide show
  1. sapiopycommons/ai/__init__.py +0 -0
  2. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.py +43 -0
  3. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.pyi +31 -0
  4. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2_grpc.py +24 -0
  5. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.py +123 -0
  6. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.pyi +598 -0
  7. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2_grpc.py +24 -0
  8. sapiopycommons/ai/api/plan/proto/step_output_pb2.py +45 -0
  9. sapiopycommons/ai/api/plan/proto/step_output_pb2.pyi +42 -0
  10. sapiopycommons/ai/api/plan/proto/step_output_pb2_grpc.py +24 -0
  11. sapiopycommons/ai/api/plan/proto/step_pb2.py +43 -0
  12. sapiopycommons/ai/api/plan/proto/step_pb2.pyi +43 -0
  13. sapiopycommons/ai/api/plan/proto/step_pb2_grpc.py +24 -0
  14. sapiopycommons/ai/api/plan/script/proto/script_pb2.py +53 -0
  15. sapiopycommons/ai/api/plan/script/proto/script_pb2.pyi +99 -0
  16. sapiopycommons/ai/api/plan/script/proto/script_pb2_grpc.py +153 -0
  17. sapiopycommons/ai/api/plan/tool/proto/entry_pb2.py +57 -0
  18. sapiopycommons/ai/api/plan/tool/proto/entry_pb2.pyi +96 -0
  19. sapiopycommons/ai/api/plan/tool/proto/entry_pb2_grpc.py +24 -0
  20. sapiopycommons/ai/api/plan/tool/proto/tool_pb2.py +67 -0
  21. sapiopycommons/ai/api/plan/tool/proto/tool_pb2.pyi +220 -0
  22. sapiopycommons/ai/api/plan/tool/proto/tool_pb2_grpc.py +154 -0
  23. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.py +39 -0
  24. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.pyi +32 -0
  25. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2_grpc.py +24 -0
  26. sapiopycommons/ai/protobuf_utils.py +454 -0
  27. sapiopycommons/ai/tool_service_base.py +790 -0
  28. sapiopycommons/callbacks/callback_util.py +64 -116
  29. sapiopycommons/callbacks/field_builder.py +0 -2
  30. sapiopycommons/chem/IndigoMolecules.py +1 -29
  31. sapiopycommons/chem/Molecules.py +3 -3
  32. sapiopycommons/customreport/auto_pagers.py +1 -2
  33. sapiopycommons/customreport/term_builder.py +1 -1
  34. sapiopycommons/datatype/pseudo_data_types.py +326 -349
  35. sapiopycommons/eln/experiment_handler.py +719 -336
  36. sapiopycommons/eln/plate_designer.py +2 -7
  37. sapiopycommons/files/file_util.py +4 -4
  38. sapiopycommons/general/accession_service.py +2 -2
  39. sapiopycommons/general/aliases.py +1 -4
  40. sapiopycommons/processtracking/custom_workflow_handler.py +1 -2
  41. sapiopycommons/recordmodel/record_handler.py +317 -657
  42. sapiopycommons/rules/eln_rule_handler.py +1 -8
  43. sapiopycommons/rules/on_save_rule_handler.py +1 -8
  44. sapiopycommons/webhook/webhook_handlers.py +0 -3
  45. sapiopycommons/webhook/webservice_handlers.py +2 -2
  46. {sapiopycommons-2025.6.11a561.dist-info → sapiopycommons-2025.6.18a563.dist-info}/METADATA +2 -2
  47. sapiopycommons-2025.6.18a563.dist-info/RECORD +91 -0
  48. sapiopycommons/eln/experiment_cache.py +0 -188
  49. sapiopycommons/eln/experiment_step_factory.py +0 -476
  50. sapiopycommons/eln/step_creation.py +0 -236
  51. sapiopycommons/general/data_structure_util.py +0 -115
  52. sapiopycommons-2025.6.11a561.dist-info/RECORD +0 -68
  53. {sapiopycommons-2025.6.11a561.dist-info → sapiopycommons-2025.6.18a563.dist-info}/WHEEL +0 -0
  54. {sapiopycommons-2025.6.11a561.dist-info → sapiopycommons-2025.6.18a563.dist-info}/licenses/LICENSE +0 -0
@@ -4,7 +4,7 @@ import io
4
4
  import re
5
5
  import warnings
6
6
  from copy import copy
7
- from typing import Iterable, TypeAlias, Any, Callable, Container, Collection
7
+ from typing import Iterable, TypeAlias, Any, Callable
8
8
  from weakref import WeakValueDictionary
9
9
 
10
10
  from requests import ReadTimeout
@@ -38,18 +38,11 @@ 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
42
41
  from sapiopycommons.recordmodel.record_handler import RecordHandler
43
42
 
44
43
  DataTypeLayoutIdentifier: TypeAlias = DataTypeLayout | str | None
45
44
 
46
45
 
47
- # CR-47521: Updated various parameter type hints from list or Iterable to more specific type hints.
48
- # If we need to iterate over the parameter, then it is Iterable.
49
- # If we need to see if the parameter contains a value, then it is Container.
50
- # If the length/size of the parameter is needed, then it is Collection.
51
- # If we need to access the parameter by an index, then it is Sequence. (This excludes sets and dictionaries, so it's
52
- # probably better to accept a Collection then cast the parameter to a list if you need to get an element from it.)
53
46
  class CallbackUtil:
54
47
  user: SapioUser
55
48
  callback: ClientCallback
@@ -200,7 +193,7 @@ class CallbackUtil:
200
193
  def ok_cancel_dialog(self, title: str, msg: str, default_ok: bool = True) -> bool:
201
194
  """
202
195
  Create an option dialog where the only options are "OK" and "Cancel". Doesn't allow the user to cancel the
203
- dialog using the X in the top right corner.
196
+ dialog using the X at the top right corner.
204
197
 
205
198
  :param title: The title of the dialog.
206
199
  :param msg: The message to display in the dialog. This can be formatted using HTML elements.
@@ -212,7 +205,7 @@ class CallbackUtil:
212
205
  def yes_no_dialog(self, title: str, msg: str, default_yes: bool = True) -> bool:
213
206
  """
214
207
  Create an option dialog where the only options are "Yes" and "No". Doesn't allow the user to cancel the
215
- dialog using the X in the top right corner.
208
+ dialog using the X at the top right corner.
216
209
 
217
210
  :param title: The title of the dialog.
218
211
  :param msg: The message to display in the dialog. This can be formatted using HTML elements.
@@ -265,7 +258,7 @@ class CallbackUtil:
265
258
  def form_dialog(self,
266
259
  title: str,
267
260
  msg: str,
268
- fields: Iterable[AbstractVeloxFieldDefinition],
261
+ fields: list[AbstractVeloxFieldDefinition],
269
262
  values: FieldMap = None,
270
263
  column_positions: dict[str, tuple[int, int]] = None,
271
264
  *,
@@ -303,7 +296,7 @@ class CallbackUtil:
303
296
  def record_form_dialog(self,
304
297
  title: str,
305
298
  msg: str,
306
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
299
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
307
300
  record: SapioRecord,
308
301
  column_positions: dict[str, tuple[int, int]] | None = None,
309
302
  editable=None,
@@ -376,7 +369,7 @@ class CallbackUtil:
376
369
  def set_record_form_dialog(self,
377
370
  title: str,
378
371
  msg: str,
379
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
372
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
380
373
  record: SapioRecord,
381
374
  column_positions: dict[str, tuple[int, int]] | None = None,
382
375
  *,
@@ -419,7 +412,7 @@ class CallbackUtil:
419
412
  def create_record_form_dialog(self,
420
413
  title: str,
421
414
  msg: str,
422
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
415
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
423
416
  wrapper_type: type[WrappedType] | str,
424
417
  column_positions: dict[str, tuple[int, int]] | None = None,
425
418
  *,
@@ -625,14 +618,14 @@ class CallbackUtil:
625
618
  def table_dialog(self,
626
619
  title: str,
627
620
  msg: str,
628
- fields: Iterable[AbstractVeloxFieldDefinition],
629
- values: Iterable[FieldMap],
621
+ fields: list[AbstractVeloxFieldDefinition],
622
+ values: list[FieldMap],
630
623
  *,
631
624
  data_type: DataTypeIdentifier = "Default",
632
625
  display_name: str | None = None,
633
626
  plural_display_name: str | None = None,
634
627
  group_by: FieldIdentifier | None = None,
635
- image_data: Iterable[bytes] | None = None) -> list[FieldMap]:
628
+ image_data: list[bytes] | None = None) -> list[FieldMap]:
636
629
  """
637
630
  Create a table dialog where the user may input data into the fields of the table. Requires that the caller
638
631
  provide the definitions of every field in the table.
@@ -667,7 +660,7 @@ class CallbackUtil:
667
660
  temp_dt.record_image_assignable = bool(image_data)
668
661
 
669
662
  # Send the request to the user.
670
- request = TableEntryDialogRequest(title, msg, temp_dt, list(values),
663
+ request = TableEntryDialogRequest(title, msg, temp_dt, values,
671
664
  record_image_data_list=image_data, group_by_field=group_by,
672
665
  width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
673
666
  response: list[FieldMap] = self.__handle_dialog_request(request, self.callback.show_table_entry_dialog)
@@ -676,15 +669,14 @@ class CallbackUtil:
676
669
  def record_table_dialog(self,
677
670
  title: str,
678
671
  msg: str,
679
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
680
- records: Iterable[SapioRecord],
672
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
673
+ records: list[SapioRecord],
681
674
  editable=None,
682
675
  *,
683
676
  default_modifier: FieldModifier | None = None,
684
677
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
685
678
  group_by: FieldIdentifier | None = None,
686
- image_data: Iterable[bytes] | None = None,
687
- index_field: str | None = None) -> list[FieldMap]:
679
+ image_data: list[bytes] | None = None) -> list[FieldMap]:
688
680
  """
689
681
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
690
682
  a given list of records of a singular type.
@@ -714,12 +706,6 @@ class CallbackUtil:
714
706
  The user may remove this grouping if they want to.
715
707
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
716
708
  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.
723
709
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
724
710
  value from the user for that field for each row.
725
711
  """
@@ -731,10 +717,7 @@ class CallbackUtil:
731
717
  if not records:
732
718
  raise SapioException("No records provided.")
733
719
  data_type: str = AliasUtil.to_singular_data_type_name(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)
720
+ field_map_list: list[FieldMap] = AliasUtil.to_field_map_list(records)
738
721
 
739
722
  # Convert the group_by parameter to a field name.
740
723
  if group_by is not None:
@@ -767,13 +750,13 @@ class CallbackUtil:
767
750
  def set_record_table_dialog(self,
768
751
  title: str,
769
752
  msg: str,
770
- fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
771
- records: Iterable[SapioRecord],
753
+ fields: list[FieldValue] | DataTypeLayoutIdentifier,
754
+ records: list[SapioRecord],
772
755
  *,
773
756
  default_modifier: FieldModifier | None = None,
774
757
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
775
758
  group_by: FieldIdentifier | None = None,
776
- image_data: Iterable[bytes] | None = None):
759
+ image_data: list[bytes] | None = None):
777
760
  """
778
761
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
779
762
  a given list of records of a singular type. After the user submits this dialog, the values that the user
@@ -804,30 +787,25 @@ class CallbackUtil:
804
787
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
805
788
  the image data list corresponds to the element at the same index in the records list.
806
789
  """
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()}"
810
790
  results: list[FieldMap] = self.record_table_dialog(title, msg, fields, records,
811
791
  default_modifier=default_modifier,
812
792
  field_modifiers=field_modifiers,
813
- group_by=group_by, image_data=image_data,
814
- index_field=index_field)
793
+ group_by=group_by, image_data=image_data)
815
794
  records_by_id: dict[int, SapioRecord] = self.rec_handler.map_by_id(records)
816
795
  for result in results:
817
- index: int = result.pop(index_field)
818
- records_by_id[index].set_field_values(result)
796
+ records_by_id[result["RecordId"]].set_field_values(result)
819
797
 
820
798
  def create_record_table_dialog(self,
821
799
  title: str,
822
800
  msg: str,
823
- fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
801
+ fields: list[FieldValue] | DataTypeLayoutIdentifier,
824
802
  wrapper_type: type[WrappedType] | str,
825
803
  count: int | tuple[int, int],
826
804
  *,
827
805
  default_modifier: FieldModifier | None = None,
828
806
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
829
807
  group_by: FieldIdentifier | None = None,
830
- image_data: Iterable[bytes] | None = None,
808
+ image_data: list[bytes] | None = None,
831
809
  require_input: bool = False,
832
810
  repeat_message: str | None = "Please provide a value to continue.") \
833
811
  -> list[WrappedType] | list[PyRecordModel]:
@@ -882,15 +860,14 @@ class CallbackUtil:
882
860
  def record_adaptive_dialog(self,
883
861
  title: str,
884
862
  msg: str,
885
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
886
- records: Collection[SapioRecord],
863
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
864
+ records: list[SapioRecord],
887
865
  *,
888
866
  default_modifier: FieldModifier | None = None,
889
867
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
890
868
  column_positions: dict[str, tuple[int, int]] | None = None,
891
869
  group_by: FieldIdentifier | None = None,
892
- image_data: Iterable[bytes] | None = None,
893
- index_field: str | None = None) -> list[FieldMap]:
870
+ image_data: list[bytes] | None = None) -> list[FieldMap]:
894
871
  """
895
872
  Create a dialog where the user may input data into the specified fields. The dialog is constructed from
896
873
  a given list of records of a singular type.
@@ -926,12 +903,6 @@ class CallbackUtil:
926
903
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
927
904
  the image data list corresponds to the element at the same index in the records list. Only used if the
928
905
  adaptive dialog becomes a table.
929
- :param index_field: If provided, the returned field maps will contain a field with this name that is equal to
930
- the record ID of the record at the same index in the records list. This can be used to map the results
931
- back to the original records. This is used instead of using a RecordId field, as the RecordId field has
932
- special behavior in the system that can cause issues if the given records are uncommitted record models
933
- with negative record IDs, meaning we don't want to have a RecordId field in the field maps provided to the
934
- system. Only used if the adaptive dialog becomes a table.
935
906
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
936
907
  value from the user for that field for each row. Even if a form was displayed, the field values will still
937
908
  be returned in a list.
@@ -940,23 +911,23 @@ class CallbackUtil:
940
911
  if not count:
941
912
  raise SapioException("No records provided.")
942
913
  if count == 1:
943
- return [self.record_form_dialog(title, msg, fields, list(records)[0], column_positions,
914
+ return [self.record_form_dialog(title, msg, fields, records[0], column_positions,
944
915
  default_modifier=default_modifier, field_modifiers=field_modifiers)]
945
916
  return self.record_table_dialog(title, msg, fields, records,
946
917
  default_modifier=default_modifier, field_modifiers=field_modifiers,
947
- group_by=group_by, image_data=image_data, index_field=index_field)
918
+ group_by=group_by, image_data=image_data)
948
919
 
949
920
  def set_record_adaptive_dialog(self,
950
921
  title: str,
951
922
  msg: str,
952
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
953
- records: Collection[SapioRecord],
923
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
924
+ records: list[SapioRecord],
954
925
  *,
955
926
  default_modifier: FieldModifier | None = None,
956
927
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
957
928
  column_positions: dict[str, tuple[int, int]] | None = None,
958
929
  group_by: FieldIdentifier | None = None,
959
- image_data: Iterable[bytes] | None = None) -> None:
930
+ image_data: list[bytes] | None = None) -> None:
960
931
  """
961
932
  Create a dialog where the user may input data into the fields of the dialog. The dialog is constructed from
962
933
  a given list of records of a singular type. After the user submits this dialog, the values that the user
@@ -998,7 +969,7 @@ class CallbackUtil:
998
969
  if not count:
999
970
  raise SapioException("No records provided.")
1000
971
  if count == 1:
1001
- self.set_record_form_dialog(title, msg, fields, list(records)[0], column_positions,
972
+ self.set_record_form_dialog(title, msg, fields, records[0], column_positions,
1002
973
  default_modifier=default_modifier, field_modifiers=field_modifiers)
1003
974
  else:
1004
975
  self.set_record_table_dialog(title, msg, fields, records,
@@ -1008,7 +979,7 @@ class CallbackUtil:
1008
979
  def create_record_adaptive_dialog(self,
1009
980
  title: str,
1010
981
  msg: str,
1011
- fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
982
+ fields: list[FieldValue] | DataTypeLayoutIdentifier,
1012
983
  wrapper_type: type[WrappedType] | str,
1013
984
  count: int | tuple[int, int],
1014
985
  *,
@@ -1016,7 +987,7 @@ class CallbackUtil:
1016
987
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
1017
988
  column_positions: dict[str, tuple[int, int]] | None = None,
1018
989
  group_by: FieldIdentifier | None = None,
1019
- image_data: Iterable[bytes] | None = None,
990
+ image_data: list[bytes] | None = None,
1020
991
  require_input: bool = False,
1021
992
  repeat_message: str | None = "Please provide a value to continue.") \
1022
993
  -> list[WrappedType]:
@@ -1079,8 +1050,8 @@ class CallbackUtil:
1079
1050
  def multi_type_table_dialog(self,
1080
1051
  title: str,
1081
1052
  msg: str,
1082
- fields: Iterable[tuple[DataTypeIdentifier, FieldIdentifier] | AbstractVeloxFieldDefinition],
1083
- row_contents: Iterable[Iterable[SapioRecord | FieldMap]],
1053
+ fields: list[tuple[DataTypeIdentifier, FieldIdentifier] | AbstractVeloxFieldDefinition],
1054
+ row_contents: list[list[SapioRecord | FieldMap]],
1084
1055
  *,
1085
1056
  default_modifier: FieldModifier | None = None,
1086
1057
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
@@ -1216,7 +1187,7 @@ class CallbackUtil:
1216
1187
  if rec is None:
1217
1188
  continue
1218
1189
  # Map records to their data type name. Map field maps to Default.
1219
- dt: str = "Default" if isinstance(rec, dict) else AliasUtil.to_data_type_name(rec)
1190
+ dt: str = "Default" if isinstance(rec, dict) else AliasUtil.to_data_type_names(rec)
1220
1191
  # Warn if the same data type name appears more than once.
1221
1192
  if dt in row_records:
1222
1193
  raise SapioException(f"The data type \"{dt}\" appears more than once in the given row contents.")
@@ -1255,7 +1226,7 @@ class CallbackUtil:
1255
1226
  layout: DataTypeLayoutIdentifier = None,
1256
1227
  minimized: bool = False,
1257
1228
  access_level: FormAccessLevel | None = None,
1258
- plugin_path_list: Iterable[str] | None = None) -> None:
1229
+ plugin_path_list: list[str] | None = None) -> None:
1259
1230
  """
1260
1231
  Create an IDV dialog for the given record. This IDV may use an existing layout already defined in the system,
1261
1232
  and can be created to allow the user to edit the field in the IDV, or to be read-only for the user to review.
@@ -1292,15 +1263,15 @@ class CallbackUtil:
1292
1263
  # CR-47326: Allow the selection dialog functions to preselect rows/records in the table.
1293
1264
  def selection_dialog(self,
1294
1265
  msg: str,
1295
- fields: Iterable[AbstractVeloxFieldDefinition],
1296
- values: Iterable[FieldMap],
1266
+ fields: list[AbstractVeloxFieldDefinition],
1267
+ values: list[FieldMap],
1297
1268
  multi_select: bool = True,
1298
- preselected_rows: Iterable[FieldMap | RecordIdentifier] | None = None,
1269
+ preselected_rows: list[FieldMap | RecordIdentifier] | None = None,
1299
1270
  *,
1300
1271
  data_type: DataTypeIdentifier = "Default",
1301
1272
  display_name: str | None = None,
1302
1273
  plural_display_name: str | None = None,
1303
- image_data: Iterable[bytes] | None = None,
1274
+ image_data: list[bytes] | None = None,
1304
1275
  require_selection: bool = False,
1305
1276
  repeat_message: str | None = "Please provide a selection to continue.") -> list[FieldMap]:
1306
1277
  """
@@ -1358,7 +1329,6 @@ class CallbackUtil:
1358
1329
 
1359
1330
  # Add a RecordId definition to the fields if one is not already present. This is necessary for the
1360
1331
  # pre-selected records parameter to function.
1361
- fields = list(fields)
1362
1332
  if "RecordId" not in [x.data_field_name for x in fields]:
1363
1333
  builder = FieldBuilder(data_type)
1364
1334
  fields.append(builder.long_field("RecordId", abstract_info=AnyFieldInfo(visible=False)))
@@ -1368,7 +1338,7 @@ class CallbackUtil:
1368
1338
  temp_dt.record_image_assignable = bool(image_data)
1369
1339
 
1370
1340
  # Send the request to the user.
1371
- request = TempTableSelectionRequest(temp_dt, msg, list(values), image_data, preselected_rows, multi_select)
1341
+ request = TempTableSelectionRequest(temp_dt, msg, values, image_data, preselected_rows, multi_select)
1372
1342
  # If require_selection is true, repeat the request if the user didn't make a selection.
1373
1343
  while True:
1374
1344
  response: list[FieldMap] = self.__handle_dialog_request(request,
@@ -1381,12 +1351,12 @@ class CallbackUtil:
1381
1351
 
1382
1352
  def record_selection_dialog(self,
1383
1353
  msg: str,
1384
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
1385
- records: Iterable[SapioRecord],
1354
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
1355
+ records: list[SapioRecord],
1386
1356
  multi_select: bool = True,
1387
- preselected_records: Iterable[RecordIdentifier] | None = None,
1357
+ preselected_records: list[RecordIdentifier] | None = None,
1388
1358
  *,
1389
- image_data: Iterable[bytes] | None = None,
1359
+ image_data: list[bytes] | None = None,
1390
1360
  require_selection: bool = False,
1391
1361
  repeat_message: str | None = "Please provide a selection to continue.") \
1392
1362
  -> list[SapioRecord]:
@@ -1468,12 +1438,12 @@ class CallbackUtil:
1468
1438
  msg: str,
1469
1439
  multi_select: bool = True,
1470
1440
  only_key_fields: bool = False,
1471
- search_types: Iterable[SearchType] | None = None,
1441
+ search_types: list[SearchType] | None = None,
1472
1442
  scan_criteria: ScanToSelectCriteria | None = None,
1473
1443
  custom_search: CustomReport | CustomReportCriteria | str | None = None,
1474
- preselected_records: Iterable[RecordIdentifier] | None = None,
1475
- record_blacklist: Iterable[RecordIdentifier] | None = None,
1476
- record_whitelist: Iterable[RecordIdentifier] | None = None,
1444
+ preselected_records: list[RecordIdentifier] | None = None,
1445
+ record_blacklist: list[RecordIdentifier] | None = None,
1446
+ record_whitelist: list[RecordIdentifier] | None = None,
1477
1447
  allow_creation: bool = False,
1478
1448
  default_creation_number: int = 1,
1479
1449
  *,
@@ -1563,7 +1533,7 @@ class CallbackUtil:
1563
1533
  title: str,
1564
1534
  msg: str,
1565
1535
  show_comment: bool = True,
1566
- additional_fields: Iterable[AbstractVeloxFieldDefinition] | None = None,
1536
+ additional_fields: list[AbstractVeloxFieldDefinition] | None = None,
1567
1537
  *,
1568
1538
  require_authentication: bool = False) -> ESigningResponsePojo:
1569
1539
  """
@@ -1602,7 +1572,7 @@ class CallbackUtil:
1602
1572
  popup_type=PopupType.Error)
1603
1573
  return response
1604
1574
 
1605
- def request_file(self, title: str, exts: Iterable[str] | None = None,
1575
+ def request_file(self, title: str, exts: list[str] | None = None,
1606
1576
  show_image_editor: bool = False, show_camera_button: bool = False) -> tuple[str, bytes]:
1607
1577
  """
1608
1578
  Request a single file from the user.
@@ -1635,7 +1605,7 @@ class CallbackUtil:
1635
1605
  self.__verify_file(file_path, sink.data, exts)
1636
1606
  return file_path, sink.data
1637
1607
 
1638
- def request_files(self, title: str, exts: Iterable[str] | None = None,
1608
+ def request_files(self, title: str, exts: list[str] | None = None,
1639
1609
  show_image_editor: bool = False, show_camera_button: bool = False) -> dict[str, bytes]:
1640
1610
  """
1641
1611
  Request multiple files from the user.
@@ -1667,7 +1637,7 @@ class CallbackUtil:
1667
1637
  return ret_dict
1668
1638
 
1669
1639
  @staticmethod
1670
- def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: Iterable[str]) -> None:
1640
+ def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]) -> None:
1671
1641
  """
1672
1642
  Verify that the provided file was read (i.e. the file path and file bytes aren't None or empty) and that it
1673
1643
  has the correct file extension. Raises a user error exception if something about the file is incorrect.
@@ -1678,7 +1648,7 @@ class CallbackUtil:
1678
1648
  """
1679
1649
  if file_path is None or len(file_path) == 0 or file_bytes is None or len(file_bytes) == 0:
1680
1650
  raise SapioUserErrorException("Empty file provided or file unable to be read.")
1681
- if allowed_extensions:
1651
+ if len(allowed_extensions) != 0:
1682
1652
  matches: bool = False
1683
1653
  for ext in allowed_extensions:
1684
1654
  if file_path.endswith("." + ext.lstrip(".")):
@@ -1695,8 +1665,8 @@ class CallbackUtil:
1695
1665
  :param file_name: The name of the file.
1696
1666
  :param file_data: The data of the file, provided as either a string or as a bytes array.
1697
1667
  """
1698
- with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as data:
1699
- self.callback.send_file(file_name, False, data)
1668
+ data = io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data)
1669
+ self.callback.send_file(file_name, False, data)
1700
1670
 
1701
1671
  def write_zip_file(self, zip_name: str, files: dict[str, str | bytes]) -> None:
1702
1672
  """
@@ -1705,33 +1675,12 @@ class CallbackUtil:
1705
1675
  :param zip_name: The name of the zip file.
1706
1676
  :param files: A dictionary of the files to add to the zip file.
1707
1677
  """
1708
- with io.BytesIO(FileUtil.zip_files(files)) as data:
1709
- self.callback.send_file(zip_name, False, data)
1710
-
1711
- @staticmethod
1712
- def __get_indexed_field_maps(records: Iterable[SapioRecord], index_field: str) -> list[FieldMap]:
1713
- """
1714
- For dialogs that accept multiple records, we may want to be able to match the returned results back to the
1715
- records that they're for. In this case, we need to add an index to each record so that we can match them back
1716
- to the original records. We can't use the RecordId field, as new record models have negative record IDs that
1717
- cause the callback dialogs to bug out if the RecordId field is present and negative.
1718
-
1719
- :param records: The records to return indexed field maps of.
1720
- :param index_field: The name of the field to use as the index. Make sure that this field doesn't exist on the
1721
- records, as then it will overwrite the existing value.
1722
- :return: A list of field maps for the records, with an index field added to each. The value of the index on
1723
- each field map is the record's record ID (even if it's a record model with a negative ID).
1724
- """
1725
- ret_val: list[FieldMap] = []
1726
- for record in records:
1727
- field_map: FieldMap = AliasUtil.to_field_map(record)
1728
- field_map[index_field] = AliasUtil.to_record_id(record)
1729
- ret_val.append(field_map)
1730
- return ret_val
1678
+ data = io.BytesIO(FileUtil.zip_files(files))
1679
+ self.callback.send_file(zip_name, False, data)
1731
1680
 
1732
1681
  @staticmethod
1733
1682
  def __temp_dt_from_field_defs(data_type: DataTypeIdentifier, display_name: str | None,
1734
- plural_display_name: str | None, fields: Iterable[AbstractVeloxFieldDefinition],
1683
+ plural_display_name: str | None, fields: list[AbstractVeloxFieldDefinition],
1735
1684
  column_positions: dict[str, tuple[int, int]] | None) -> TemporaryDataType:
1736
1685
  """
1737
1686
  Construct a Temporary Data Type definition from a provided list of field definitions for use in a callback.
@@ -1765,7 +1714,7 @@ class CallbackUtil:
1765
1714
  builder.add_field(field_def, column, span)
1766
1715
  return builder.get_temporary_data_type()
1767
1716
 
1768
- def __temp_dt_from_field_names(self, data_type: str, fields: Iterable[FieldIdentifier | FieldFilterCriteria],
1717
+ def __temp_dt_from_field_names(self, data_type: str, fields: list[FieldIdentifier | FieldFilterCriteria],
1769
1718
  column_positions: dict[str, tuple[int, int]] | None,
1770
1719
  default_modifier: FieldModifier, field_modifiers: dict[str, FieldModifier]) \
1771
1720
  -> TemporaryDataType:
@@ -1782,7 +1731,6 @@ class CallbackUtil:
1782
1731
 
1783
1732
  # Determine if any FieldFilterCriteria were provided. If so, remove them from the fields list so that it
1784
1733
  # contains only field identifiers.
1785
- fields = list(fields)
1786
1734
  filter_criteria: list[FieldFilterCriteria] = [x for x in fields if isinstance(x, FieldFilterCriteria)]
1787
1735
  for criteria in filter_criteria:
1788
1736
  fields.remove(criteria)
@@ -2035,15 +1983,15 @@ class FieldFilterCriteria:
2035
1983
  key_field: bool | None
2036
1984
  identifier: bool | None
2037
1985
  system_field: bool | None
2038
- field_types: Container[FieldType] | None
2039
- not_field_types: Container[FieldType] | None
1986
+ field_types: list[FieldType] | None
1987
+ not_field_types: list[FieldType] | None
2040
1988
  matches_tag: str | None
2041
1989
  contains_tag: str | None
2042
1990
  regex_tag: str | re.Pattern[str] | None
2043
1991
 
2044
1992
  def __init__(self, *, required: bool | None = None, editable: bool | None = None, key_field: bool | None = None,
2045
1993
  identifier: bool | None = None, system_field: bool | None = None,
2046
- field_types: Container[FieldType] | None = None, not_field_types: Container[FieldType] | None = None,
1994
+ field_types: list[FieldType] | None = None, not_field_types: list[FieldType] | None = None,
2047
1995
  matches_tag: str | None = None, contains_tag: str | None = None,
2048
1996
  regex_tag: str | re.Pattern[str] | None = None):
2049
1997
  """
@@ -443,8 +443,6 @@ class FieldBuilder:
443
443
  raise SapioException("Unable to set multiple list modes at once for a selection list.")
444
444
  # Static values don't have a list mode. Evaluate this last so that the multiple list modes check doesn't
445
445
  # need to be more complex.
446
- # PR-47531: Even though static values don't use an existing list mode, a list mode must still be set.
447
- list_mode = ListMode.USER
448
446
 
449
447
  if not list_mode and static_values is None:
450
448
  raise SapioException("A list mode must be chosen for selection list fields.")
@@ -10,36 +10,9 @@ indigo.setOption("render-stereo-style", "ext")
10
10
  indigo.setOption("aromaticity-model", "generic")
11
11
  indigo.setOption("render-coloring", True)
12
12
  indigo.setOption("molfile-saving-mode", "3000")
13
- indigo.setOption("dearomatize-verification", False)
14
13
  indigo_inchi = IndigoInchi(indigo)
15
14
 
16
15
 
17
- def get_aromatic_dearomatic_forms(m: IndigoObject):
18
- """
19
- Get the aromatic and dearomatic forms of the molecule. Retain the original form if it's not inversible.
20
- Inversible: after aromatic-dearomatic-aromatic transformation, the molecule is the same as the first aromatic transformation.
21
- :param m: molecule from indigo.
22
- :return: pair of indigo objects, first is aromatic, second is dearomatic.
23
- """
24
- try:
25
- aromatic_reaction = m.clone()
26
- aromatic_reaction.aromatize()
27
- dearomatic_reaction = aromatic_reaction.clone()
28
- dearomatic_reaction.dearomatize()
29
- second_aromatic_reaction = dearomatic_reaction.clone()
30
- second_aromatic_reaction.aromatize()
31
- match = indigo.exactMatch(aromatic_reaction, second_aromatic_reaction)
32
- if match:
33
- return aromatic_reaction, dearomatic_reaction
34
- else:
35
- return m, dearomatic_reaction
36
- except (Exception):
37
- # If aromatization then following deromatization fails, we just skip it.
38
- dearomatic_reaction = m.clone()
39
- dearomatic_reaction.dearomatize()
40
- return m, dearomatic_reaction
41
-
42
-
43
16
  # Function to process dative bonds in a molecule
44
17
  # Returns True if at least one dative bond (_BOND_COORDINATION) was removed
45
18
  def remove_dative_bonds_in_mol(molecule: IndigoObject) -> bool:
@@ -78,8 +51,7 @@ def remove_dative_in_reaction(reaction: IndigoObject) -> bool:
78
51
  :param reaction: The reaction to remove dative bonds.
79
52
  :return: Whether there are any dative bonds in the reaction that were removed.
80
53
  """
81
- reactant_dative_removed: bool = any(
82
- remove_dative_bonds_in_mol(reactant) for reactant in reaction.iterateReactants())
54
+ reactant_dative_removed: bool = any(remove_dative_bonds_in_mol(reactant) for reactant in reaction.iterateReactants())
83
55
  product_dative_removed: bool = any(remove_dative_bonds_in_mol(product) for product in reaction.iterateProducts())
84
56
  return reactant_dative_removed or product_dative_removed
85
57
 
@@ -1,6 +1,6 @@
1
1
  # Author Yechen Qiao
2
2
  # Common Molecule Utilities for Molecule Transfers with Sapio
3
- from indigo import IndigoObject
3
+
4
4
  from rdkit import Chem
5
5
  from rdkit.Chem import Crippen, MolToInchi
6
6
  from rdkit.Chem import Descriptors
@@ -10,7 +10,7 @@ from rdkit.Chem.MolStandardize import rdMolStandardize
10
10
  from rdkit.Chem.SaltRemover import SaltRemover
11
11
  from rdkit.Chem.rdchem import Mol, RWMol, Bond
12
12
 
13
- from sapiopycommons.chem.IndigoMolecules import indigo, renderer, indigo_inchi, get_aromatic_dearomatic_forms
13
+ from sapiopycommons.chem.IndigoMolecules import indigo, renderer, indigo_inchi
14
14
 
15
15
  metal_disconnector = rdMolStandardize.MetalDisconnector()
16
16
  tautomer_params = Chem.MolStandardize.rdMolStandardize.CleanupParameters()
@@ -247,7 +247,7 @@ def mol_to_sapio_substance(mol: Mol, include_stereoisomers=False,
247
247
  molecule["image"] = None
248
248
  # We need to test the INCHI can be loaded back to indigo.
249
249
  indigo_mol = indigo.loadMolecule(molBlock)
250
- indigo_mol = get_aromatic_dearomatic_forms(indigo_mol)[0] # Get the aromatic form of the molecule.
250
+ indigo_mol.aromatize()
251
251
  if enhanced_stereo:
252
252
  # Remove enhanced stereo layer when generating InChI as the stereo hash is generated separately for reg.
253
253
  Chem.CanonicalizeEnhancedStereo(inchi_mol)
@@ -6,7 +6,6 @@ from sapiopylib.rest.CustomReportService import CustomReportManager
6
6
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
7
7
  from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, CustomReport, RawReportTerm, ReportColumn
8
8
  from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
9
- # noinspection PyProtectedMember
10
9
  from sapiopylib.rest.utils.autopaging import SapioPyAutoPager, PagerResultCriteriaType, _default_report_page_size, \
11
10
  _default_record_page_size
12
11
  from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
@@ -154,7 +153,7 @@ class _RecordReportPagerBase(SapioPyAutoPager[CustomReportCriteria, WrappedType
154
153
  if id_index == -1:
155
154
  raise SapioException(f"This report does not contain a Record ID column for the given record model type "
156
155
  f"{self._data_type}.")
157
- ids: set[int] = {row[id_index] for row in report.result_table}
156
+ ids: list[int] = [row[id_index] for row in report.result_table]
158
157
  for row in self._rec_handler.query_models_by_id(self._query_type, ids, page_size=report.page_size):
159
158
  queue.put(row)
160
159
  if report.has_next_page:
@@ -279,7 +279,7 @@ class TermBuilder:
279
279
 
280
280
  :param a: The first term in the operation.
281
281
  :param b: The second term in the operation.
282
- :param is_negated: Whether the returned term should be negated (i.e. turn this into an XNOR operation).
282
+ :param is_negated: Whether the returned term should be negated (i.e. turn this into an xnor operation).
283
283
  :return: A composite report term for "A xor B".
284
284
  """
285
285
  return TermBuilder.and_terms(TermBuilder.or_terms(a, b),