sapiopycommons 2025.3.27a461__py3-none-any.whl → 2025.4.3a467__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,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
7
+ from typing import Iterable, TypeAlias, Any, Callable, Container, Collection
8
8
  from weakref import WeakValueDictionary
9
9
 
10
10
  from requests import ReadTimeout
@@ -43,6 +43,12 @@ from sapiopycommons.recordmodel.record_handler import RecordHandler
43
43
  DataTypeLayoutIdentifier: TypeAlias = DataTypeLayout | str | None
44
44
 
45
45
 
46
+ # CR-47521: Updated various parameter type hints from list or Iterable to more specific type hints.
47
+ # If we need to iterate over the parameter, then it is Iterable.
48
+ # If we need to see if the parameter contains a value, then it is Container.
49
+ # If the length/size of the parameter is needed, then it is Collection.
50
+ # If we need to access the parameter by an index, then it is Sequence. (This excludes sets and dictionaries, so it's
51
+ # probably better to accept a Collection then cast the parameter to a list if you need to get an element from it.)
46
52
  class CallbackUtil:
47
53
  user: SapioUser
48
54
  callback: ClientCallback
@@ -193,7 +199,7 @@ class CallbackUtil:
193
199
  def ok_cancel_dialog(self, title: str, msg: str, default_ok: bool = True) -> bool:
194
200
  """
195
201
  Create an option dialog where the only options are "OK" and "Cancel". Doesn't allow the user to cancel the
196
- dialog using the X at the top right corner.
202
+ dialog using the X in the top right corner.
197
203
 
198
204
  :param title: The title of the dialog.
199
205
  :param msg: The message to display in the dialog. This can be formatted using HTML elements.
@@ -205,7 +211,7 @@ class CallbackUtil:
205
211
  def yes_no_dialog(self, title: str, msg: str, default_yes: bool = True) -> bool:
206
212
  """
207
213
  Create an option dialog where the only options are "Yes" and "No". Doesn't allow the user to cancel the
208
- dialog using the X at the top right corner.
214
+ dialog using the X in the top right corner.
209
215
 
210
216
  :param title: The title of the dialog.
211
217
  :param msg: The message to display in the dialog. This can be formatted using HTML elements.
@@ -258,7 +264,7 @@ class CallbackUtil:
258
264
  def form_dialog(self,
259
265
  title: str,
260
266
  msg: str,
261
- fields: list[AbstractVeloxFieldDefinition],
267
+ fields: Iterable[AbstractVeloxFieldDefinition],
262
268
  values: FieldMap = None,
263
269
  column_positions: dict[str, tuple[int, int]] = None,
264
270
  *,
@@ -296,7 +302,7 @@ class CallbackUtil:
296
302
  def record_form_dialog(self,
297
303
  title: str,
298
304
  msg: str,
299
- fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
305
+ fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
300
306
  record: SapioRecord,
301
307
  column_positions: dict[str, tuple[int, int]] | None = None,
302
308
  editable=None,
@@ -369,7 +375,7 @@ class CallbackUtil:
369
375
  def set_record_form_dialog(self,
370
376
  title: str,
371
377
  msg: str,
372
- fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
378
+ fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
373
379
  record: SapioRecord,
374
380
  column_positions: dict[str, tuple[int, int]] | None = None,
375
381
  *,
@@ -412,7 +418,7 @@ class CallbackUtil:
412
418
  def create_record_form_dialog(self,
413
419
  title: str,
414
420
  msg: str,
415
- fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
421
+ fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
416
422
  wrapper_type: type[WrappedType] | str,
417
423
  column_positions: dict[str, tuple[int, int]] | None = None,
418
424
  *,
@@ -618,14 +624,14 @@ class CallbackUtil:
618
624
  def table_dialog(self,
619
625
  title: str,
620
626
  msg: str,
621
- fields: list[AbstractVeloxFieldDefinition],
622
- values: list[FieldMap],
627
+ fields: Iterable[AbstractVeloxFieldDefinition],
628
+ values: Iterable[FieldMap],
623
629
  *,
624
630
  data_type: DataTypeIdentifier = "Default",
625
631
  display_name: str | None = None,
626
632
  plural_display_name: str | None = None,
627
633
  group_by: FieldIdentifier | None = None,
628
- image_data: list[bytes] | None = None) -> list[FieldMap]:
634
+ image_data: Iterable[bytes] | None = None) -> list[FieldMap]:
629
635
  """
630
636
  Create a table dialog where the user may input data into the fields of the table. Requires that the caller
631
637
  provide the definitions of every field in the table.
@@ -660,7 +666,7 @@ class CallbackUtil:
660
666
  temp_dt.record_image_assignable = bool(image_data)
661
667
 
662
668
  # Send the request to the user.
663
- request = TableEntryDialogRequest(title, msg, temp_dt, values,
669
+ request = TableEntryDialogRequest(title, msg, temp_dt, list(values),
664
670
  record_image_data_list=image_data, group_by_field=group_by,
665
671
  width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
666
672
  response: list[FieldMap] = self.__handle_dialog_request(request, self.callback.show_table_entry_dialog)
@@ -669,14 +675,14 @@ class CallbackUtil:
669
675
  def record_table_dialog(self,
670
676
  title: str,
671
677
  msg: str,
672
- fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
673
- records: list[SapioRecord],
678
+ fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
679
+ records: Iterable[SapioRecord],
674
680
  editable=None,
675
681
  *,
676
682
  default_modifier: FieldModifier | None = None,
677
683
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
678
684
  group_by: FieldIdentifier | None = None,
679
- image_data: list[bytes] | None = None) -> list[FieldMap]:
685
+ image_data: Iterable[bytes] | None = None) -> list[FieldMap]:
680
686
  """
681
687
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
682
688
  a given list of records of a singular type.
@@ -750,13 +756,13 @@ class CallbackUtil:
750
756
  def set_record_table_dialog(self,
751
757
  title: str,
752
758
  msg: str,
753
- fields: list[FieldValue] | DataTypeLayoutIdentifier,
754
- records: list[SapioRecord],
759
+ fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
760
+ records: Iterable[SapioRecord],
755
761
  *,
756
762
  default_modifier: FieldModifier | None = None,
757
763
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
758
764
  group_by: FieldIdentifier | None = None,
759
- image_data: list[bytes] | None = None):
765
+ image_data: Iterable[bytes] | None = None):
760
766
  """
761
767
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
762
768
  a given list of records of a singular type. After the user submits this dialog, the values that the user
@@ -798,14 +804,14 @@ class CallbackUtil:
798
804
  def create_record_table_dialog(self,
799
805
  title: str,
800
806
  msg: str,
801
- fields: list[FieldValue] | DataTypeLayoutIdentifier,
807
+ fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
802
808
  wrapper_type: type[WrappedType] | str,
803
809
  count: int | tuple[int, int],
804
810
  *,
805
811
  default_modifier: FieldModifier | None = None,
806
812
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
807
813
  group_by: FieldIdentifier | None = None,
808
- image_data: list[bytes] | None = None,
814
+ image_data: Iterable[bytes] | None = None,
809
815
  require_input: bool = False,
810
816
  repeat_message: str | None = "Please provide a value to continue.") \
811
817
  -> list[WrappedType] | list[PyRecordModel]:
@@ -860,14 +866,14 @@ class CallbackUtil:
860
866
  def record_adaptive_dialog(self,
861
867
  title: str,
862
868
  msg: str,
863
- fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
864
- records: list[SapioRecord],
869
+ fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
870
+ records: Collection[SapioRecord],
865
871
  *,
866
872
  default_modifier: FieldModifier | None = None,
867
873
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
868
874
  column_positions: dict[str, tuple[int, int]] | None = None,
869
875
  group_by: FieldIdentifier | None = None,
870
- image_data: list[bytes] | None = None) -> list[FieldMap]:
876
+ image_data: Iterable[bytes] | None = None) -> list[FieldMap]:
871
877
  """
872
878
  Create a dialog where the user may input data into the specified fields. The dialog is constructed from
873
879
  a given list of records of a singular type.
@@ -911,7 +917,7 @@ class CallbackUtil:
911
917
  if not count:
912
918
  raise SapioException("No records provided.")
913
919
  if count == 1:
914
- return [self.record_form_dialog(title, msg, fields, records[0], column_positions,
920
+ return [self.record_form_dialog(title, msg, fields, list(records)[0], column_positions,
915
921
  default_modifier=default_modifier, field_modifiers=field_modifiers)]
916
922
  return self.record_table_dialog(title, msg, fields, records,
917
923
  default_modifier=default_modifier, field_modifiers=field_modifiers,
@@ -920,14 +926,14 @@ class CallbackUtil:
920
926
  def set_record_adaptive_dialog(self,
921
927
  title: str,
922
928
  msg: str,
923
- fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
924
- records: list[SapioRecord],
929
+ fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
930
+ records: Collection[SapioRecord],
925
931
  *,
926
932
  default_modifier: FieldModifier | None = None,
927
933
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
928
934
  column_positions: dict[str, tuple[int, int]] | None = None,
929
935
  group_by: FieldIdentifier | None = None,
930
- image_data: list[bytes] | None = None) -> None:
936
+ image_data: Iterable[bytes] | None = None) -> None:
931
937
  """
932
938
  Create a dialog where the user may input data into the fields of the dialog. The dialog is constructed from
933
939
  a given list of records of a singular type. After the user submits this dialog, the values that the user
@@ -969,7 +975,7 @@ class CallbackUtil:
969
975
  if not count:
970
976
  raise SapioException("No records provided.")
971
977
  if count == 1:
972
- self.set_record_form_dialog(title, msg, fields, records[0], column_positions,
978
+ self.set_record_form_dialog(title, msg, fields, list(records)[0], column_positions,
973
979
  default_modifier=default_modifier, field_modifiers=field_modifiers)
974
980
  else:
975
981
  self.set_record_table_dialog(title, msg, fields, records,
@@ -979,7 +985,7 @@ class CallbackUtil:
979
985
  def create_record_adaptive_dialog(self,
980
986
  title: str,
981
987
  msg: str,
982
- fields: list[FieldValue] | DataTypeLayoutIdentifier,
988
+ fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
983
989
  wrapper_type: type[WrappedType] | str,
984
990
  count: int | tuple[int, int],
985
991
  *,
@@ -987,7 +993,7 @@ class CallbackUtil:
987
993
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
988
994
  column_positions: dict[str, tuple[int, int]] | None = None,
989
995
  group_by: FieldIdentifier | None = None,
990
- image_data: list[bytes] | None = None,
996
+ image_data: Iterable[bytes] | None = None,
991
997
  require_input: bool = False,
992
998
  repeat_message: str | None = "Please provide a value to continue.") \
993
999
  -> list[WrappedType]:
@@ -1050,8 +1056,8 @@ class CallbackUtil:
1050
1056
  def multi_type_table_dialog(self,
1051
1057
  title: str,
1052
1058
  msg: str,
1053
- fields: list[tuple[DataTypeIdentifier, FieldIdentifier] | AbstractVeloxFieldDefinition],
1054
- row_contents: list[list[SapioRecord | FieldMap]],
1059
+ fields: Iterable[tuple[DataTypeIdentifier, FieldIdentifier] | AbstractVeloxFieldDefinition],
1060
+ row_contents: Iterable[Iterable[SapioRecord | FieldMap]],
1055
1061
  *,
1056
1062
  default_modifier: FieldModifier | None = None,
1057
1063
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
@@ -1226,7 +1232,7 @@ class CallbackUtil:
1226
1232
  layout: DataTypeLayoutIdentifier = None,
1227
1233
  minimized: bool = False,
1228
1234
  access_level: FormAccessLevel | None = None,
1229
- plugin_path_list: list[str] | None = None) -> None:
1235
+ plugin_path_list: Iterable[str] | None = None) -> None:
1230
1236
  """
1231
1237
  Create an IDV dialog for the given record. This IDV may use an existing layout already defined in the system,
1232
1238
  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.
@@ -1263,15 +1269,15 @@ class CallbackUtil:
1263
1269
  # CR-47326: Allow the selection dialog functions to preselect rows/records in the table.
1264
1270
  def selection_dialog(self,
1265
1271
  msg: str,
1266
- fields: list[AbstractVeloxFieldDefinition],
1267
- values: list[FieldMap],
1272
+ fields: Iterable[AbstractVeloxFieldDefinition],
1273
+ values: Iterable[FieldMap],
1268
1274
  multi_select: bool = True,
1269
- preselected_rows: list[FieldMap | RecordIdentifier] | None = None,
1275
+ preselected_rows: Iterable[FieldMap | RecordIdentifier] | None = None,
1270
1276
  *,
1271
1277
  data_type: DataTypeIdentifier = "Default",
1272
1278
  display_name: str | None = None,
1273
1279
  plural_display_name: str | None = None,
1274
- image_data: list[bytes] | None = None,
1280
+ image_data: Iterable[bytes] | None = None,
1275
1281
  require_selection: bool = False,
1276
1282
  repeat_message: str | None = "Please provide a selection to continue.") -> list[FieldMap]:
1277
1283
  """
@@ -1329,6 +1335,7 @@ class CallbackUtil:
1329
1335
 
1330
1336
  # Add a RecordId definition to the fields if one is not already present. This is necessary for the
1331
1337
  # pre-selected records parameter to function.
1338
+ fields = list(fields)
1332
1339
  if "RecordId" not in [x.data_field_name for x in fields]:
1333
1340
  builder = FieldBuilder(data_type)
1334
1341
  fields.append(builder.long_field("RecordId", abstract_info=AnyFieldInfo(visible=False)))
@@ -1338,7 +1345,7 @@ class CallbackUtil:
1338
1345
  temp_dt.record_image_assignable = bool(image_data)
1339
1346
 
1340
1347
  # Send the request to the user.
1341
- request = TempTableSelectionRequest(temp_dt, msg, values, image_data, preselected_rows, multi_select)
1348
+ request = TempTableSelectionRequest(temp_dt, msg, list(values), image_data, preselected_rows, multi_select)
1342
1349
  # If require_selection is true, repeat the request if the user didn't make a selection.
1343
1350
  while True:
1344
1351
  response: list[FieldMap] = self.__handle_dialog_request(request,
@@ -1351,12 +1358,12 @@ class CallbackUtil:
1351
1358
 
1352
1359
  def record_selection_dialog(self,
1353
1360
  msg: str,
1354
- fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
1355
- records: list[SapioRecord],
1361
+ fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
1362
+ records: Iterable[SapioRecord],
1356
1363
  multi_select: bool = True,
1357
- preselected_records: list[RecordIdentifier] | None = None,
1364
+ preselected_records: Iterable[RecordIdentifier] | None = None,
1358
1365
  *,
1359
- image_data: list[bytes] | None = None,
1366
+ image_data: Iterable[bytes] | None = None,
1360
1367
  require_selection: bool = False,
1361
1368
  repeat_message: str | None = "Please provide a selection to continue.") \
1362
1369
  -> list[SapioRecord]:
@@ -1438,12 +1445,12 @@ class CallbackUtil:
1438
1445
  msg: str,
1439
1446
  multi_select: bool = True,
1440
1447
  only_key_fields: bool = False,
1441
- search_types: list[SearchType] | None = None,
1448
+ search_types: Iterable[SearchType] | None = None,
1442
1449
  scan_criteria: ScanToSelectCriteria | None = None,
1443
1450
  custom_search: CustomReport | CustomReportCriteria | str | None = None,
1444
- preselected_records: list[RecordIdentifier] | None = None,
1445
- record_blacklist: list[RecordIdentifier] | None = None,
1446
- record_whitelist: list[RecordIdentifier] | None = None,
1451
+ preselected_records: Iterable[RecordIdentifier] | None = None,
1452
+ record_blacklist: Iterable[RecordIdentifier] | None = None,
1453
+ record_whitelist: Iterable[RecordIdentifier] | None = None,
1447
1454
  allow_creation: bool = False,
1448
1455
  default_creation_number: int = 1,
1449
1456
  *,
@@ -1533,7 +1540,7 @@ class CallbackUtil:
1533
1540
  title: str,
1534
1541
  msg: str,
1535
1542
  show_comment: bool = True,
1536
- additional_fields: list[AbstractVeloxFieldDefinition] | None = None,
1543
+ additional_fields: Iterable[AbstractVeloxFieldDefinition] | None = None,
1537
1544
  *,
1538
1545
  require_authentication: bool = False) -> ESigningResponsePojo:
1539
1546
  """
@@ -1572,7 +1579,7 @@ class CallbackUtil:
1572
1579
  popup_type=PopupType.Error)
1573
1580
  return response
1574
1581
 
1575
- def request_file(self, title: str, exts: list[str] | None = None,
1582
+ def request_file(self, title: str, exts: Iterable[str] | None = None,
1576
1583
  show_image_editor: bool = False, show_camera_button: bool = False) -> tuple[str, bytes]:
1577
1584
  """
1578
1585
  Request a single file from the user.
@@ -1605,7 +1612,7 @@ class CallbackUtil:
1605
1612
  self.__verify_file(file_path, sink.data, exts)
1606
1613
  return file_path, sink.data
1607
1614
 
1608
- def request_files(self, title: str, exts: list[str] | None = None,
1615
+ def request_files(self, title: str, exts: Iterable[str] | None = None,
1609
1616
  show_image_editor: bool = False, show_camera_button: bool = False) -> dict[str, bytes]:
1610
1617
  """
1611
1618
  Request multiple files from the user.
@@ -1637,7 +1644,7 @@ class CallbackUtil:
1637
1644
  return ret_dict
1638
1645
 
1639
1646
  @staticmethod
1640
- def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]) -> None:
1647
+ def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: Iterable[str]) -> None:
1641
1648
  """
1642
1649
  Verify that the provided file was read (i.e. the file path and file bytes aren't None or empty) and that it
1643
1650
  has the correct file extension. Raises a user error exception if something about the file is incorrect.
@@ -1648,7 +1655,7 @@ class CallbackUtil:
1648
1655
  """
1649
1656
  if file_path is None or len(file_path) == 0 or file_bytes is None or len(file_bytes) == 0:
1650
1657
  raise SapioUserErrorException("Empty file provided or file unable to be read.")
1651
- if len(allowed_extensions) != 0:
1658
+ if allowed_extensions:
1652
1659
  matches: bool = False
1653
1660
  for ext in allowed_extensions:
1654
1661
  if file_path.endswith("." + ext.lstrip(".")):
@@ -1665,8 +1672,8 @@ class CallbackUtil:
1665
1672
  :param file_name: The name of the file.
1666
1673
  :param file_data: The data of the file, provided as either a string or as a bytes array.
1667
1674
  """
1668
- data = io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data)
1669
- self.callback.send_file(file_name, False, data)
1675
+ with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as data:
1676
+ self.callback.send_file(file_name, False, data)
1670
1677
 
1671
1678
  def write_zip_file(self, zip_name: str, files: dict[str, str | bytes]) -> None:
1672
1679
  """
@@ -1675,12 +1682,12 @@ class CallbackUtil:
1675
1682
  :param zip_name: The name of the zip file.
1676
1683
  :param files: A dictionary of the files to add to the zip file.
1677
1684
  """
1678
- data = io.BytesIO(FileUtil.zip_files(files))
1679
- self.callback.send_file(zip_name, False, data)
1685
+ with io.BytesIO(FileUtil.zip_files(files)) as data:
1686
+ self.callback.send_file(zip_name, False, data)
1680
1687
 
1681
1688
  @staticmethod
1682
1689
  def __temp_dt_from_field_defs(data_type: DataTypeIdentifier, display_name: str | None,
1683
- plural_display_name: str | None, fields: list[AbstractVeloxFieldDefinition],
1690
+ plural_display_name: str | None, fields: Iterable[AbstractVeloxFieldDefinition],
1684
1691
  column_positions: dict[str, tuple[int, int]] | None) -> TemporaryDataType:
1685
1692
  """
1686
1693
  Construct a Temporary Data Type definition from a provided list of field definitions for use in a callback.
@@ -1714,7 +1721,7 @@ class CallbackUtil:
1714
1721
  builder.add_field(field_def, column, span)
1715
1722
  return builder.get_temporary_data_type()
1716
1723
 
1717
- def __temp_dt_from_field_names(self, data_type: str, fields: list[FieldIdentifier | FieldFilterCriteria],
1724
+ def __temp_dt_from_field_names(self, data_type: str, fields: Iterable[FieldIdentifier | FieldFilterCriteria],
1718
1725
  column_positions: dict[str, tuple[int, int]] | None,
1719
1726
  default_modifier: FieldModifier, field_modifiers: dict[str, FieldModifier]) \
1720
1727
  -> TemporaryDataType:
@@ -1731,6 +1738,7 @@ class CallbackUtil:
1731
1738
 
1732
1739
  # Determine if any FieldFilterCriteria were provided. If so, remove them from the fields list so that it
1733
1740
  # contains only field identifiers.
1741
+ fields = list(fields)
1734
1742
  filter_criteria: list[FieldFilterCriteria] = [x for x in fields if isinstance(x, FieldFilterCriteria)]
1735
1743
  for criteria in filter_criteria:
1736
1744
  fields.remove(criteria)
@@ -1983,15 +1991,15 @@ class FieldFilterCriteria:
1983
1991
  key_field: bool | None
1984
1992
  identifier: bool | None
1985
1993
  system_field: bool | None
1986
- field_types: list[FieldType] | None
1987
- not_field_types: list[FieldType] | None
1994
+ field_types: Container[FieldType] | None
1995
+ not_field_types: Container[FieldType] | None
1988
1996
  matches_tag: str | None
1989
1997
  contains_tag: str | None
1990
1998
  regex_tag: str | re.Pattern[str] | None
1991
1999
 
1992
2000
  def __init__(self, *, required: bool | None = None, editable: bool | None = None, key_field: bool | None = None,
1993
2001
  identifier: bool | None = None, system_field: bool | None = None,
1994
- field_types: list[FieldType] | None = None, not_field_types: list[FieldType] | None = None,
2002
+ field_types: Container[FieldType] | None = None, not_field_types: Container[FieldType] | None = None,
1995
2003
  matches_tag: str | None = None, contains_tag: str | None = None,
1996
2004
  regex_tag: str | re.Pattern[str] | None = None):
1997
2005
  """
@@ -443,6 +443,8 @@ 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
446
448
 
447
449
  if not list_mode and static_values is None:
448
450
  raise SapioException("A list mode must be chosen for selection list fields.")
@@ -6,6 +6,7 @@ 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
9
10
  from sapiopylib.rest.utils.autopaging import SapioPyAutoPager, PagerResultCriteriaType, _default_report_page_size, \
10
11
  _default_record_page_size
11
12
  from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
@@ -153,7 +154,7 @@ class _RecordReportPagerBase(SapioPyAutoPager[CustomReportCriteria, WrappedType
153
154
  if id_index == -1:
154
155
  raise SapioException(f"This report does not contain a Record ID column for the given record model type "
155
156
  f"{self._data_type}.")
156
- ids: list[int] = [row[id_index] for row in report.result_table]
157
+ ids: set[int] = {row[id_index] for row in report.result_table}
157
158
  for row in self._rec_handler.query_models_by_id(self._query_type, ids, page_size=report.page_size):
158
159
  queue.put(row)
159
160
  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),
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ from weakref import WeakValueDictionary
4
+
5
+ from sapiopylib.rest.ELNService import ElnManager
6
+ from sapiopylib.rest.User import SapioUser
7
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition
8
+ from sapiopylib.rest.pojo.eln.ElnExperiment import ElnTemplate, TemplateExperimentQuery
9
+ from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
10
+ from sapiopylib.rest.pojo.eln.field_set import ElnFieldSetInfo
11
+ from sapiopylib.rest.pojo.eln.protocol_template import ProtocolTemplateInfo, ProtocolTemplateQuery
12
+
13
+ from sapiopycommons.general.aliases import UserIdentifier, AliasUtil
14
+ from sapiopycommons.general.exceptions import SapioException
15
+
16
+
17
+ # FR-47530: Created a class that caches experiment template and predefined field information.
18
+ class ExperimentCacheManager:
19
+ """
20
+ A class to manage the caching of experiment-related information.
21
+ """
22
+ user: SapioUser
23
+ eln_man: ElnManager
24
+
25
+ _templates: list[ElnTemplate]
26
+ """A list of experiment templates. Only cached when first accessed."""
27
+ _protocols: list[ProtocolTemplateInfo]
28
+ """A list of protocol templates. Only cached when first accessed."""
29
+ _field_sets: dict[str, ElnFieldSetInfo]
30
+ """A dictionary of field set name to field set. Only cached when first accessed."""
31
+ _field_set_fields: dict[int, list[AbstractVeloxFieldDefinition]]
32
+ """A dictionary of field set ID to field definitions. Only cached when first accessed."""
33
+ _predefined_fields: dict[str, dict[str, AbstractVeloxFieldDefinition]]
34
+ """A dictionary of ELN data type name to predefined field definitions. Only cached when first accessed."""
35
+
36
+ __instances: WeakValueDictionary[SapioUser, ExperimentCacheManager] = WeakValueDictionary()
37
+ __initialized: bool
38
+
39
+ def __new__(cls, context: UserIdentifier):
40
+ """
41
+ :param context: The current webhook context or a user object to send requests from.
42
+ """
43
+ user = AliasUtil.to_sapio_user(context)
44
+ obj = cls.__instances.get(user)
45
+ if not obj:
46
+ obj = object.__new__(cls)
47
+ obj.__initialized = False
48
+ cls.__instances[user] = obj
49
+ return obj
50
+
51
+ def __init__(self, context: UserIdentifier):
52
+ """
53
+ :param context: The current webhook context or a user object to send requests from.
54
+ """
55
+ if self.__initialized:
56
+ return
57
+ self.__initialized = True
58
+
59
+ self.user = AliasUtil.to_sapio_user(context)
60
+ self.eln_man = ElnManager(self.user)
61
+
62
+ self._field_set_fields = {}
63
+ self._predefined_fields = {}
64
+
65
+ def get_experiment_template(self, name: str, active: bool = True, version: int | None = None,
66
+ first_match: bool = False) -> ElnTemplate:
67
+ """
68
+ Get the experiment template with the given information.
69
+
70
+ :param name: The name of the template.
71
+ :param active: Whether the template is marked as active.
72
+ :param version: The version of the template to get. If None, the latest version will be returned.
73
+ :param first_match: If true, returns the first match found. If false, raises an exception.
74
+ :return: The experiment template with the given information.
75
+ """
76
+ if not hasattr(self, "_templates"):
77
+ query = TemplateExperimentQuery()
78
+ query.active_templates_only = False
79
+ query.latest_version_only = False
80
+ self._templates = self.eln_man.get_template_experiment_list(query)
81
+ return self._find_template(self._templates, name, active, version, first_match)
82
+
83
+
84
+ def get_protocol_template(self, name: str, active: bool = True, version: int | None = None,
85
+ first_match: bool = False) -> ProtocolTemplateInfo:
86
+ """
87
+ Get the protocol template with the given information. Will throw an exception if multiple templates match
88
+ the given information.
89
+
90
+ :param name: The name of the template.
91
+ :param active: Whether the template is marked as active.
92
+ :param version: The version of the template to get. If None, the latest version will be returned.
93
+ :param first_match: If true, returns the first match found. If false, raises an exception.
94
+ :return: The protocol template with the given information.
95
+ """
96
+ if not hasattr(self, "_protocols"):
97
+ query = ProtocolTemplateQuery()
98
+ query.active_templates_only = False
99
+ query.latest_version_only = False
100
+ self._protocols = self.eln_man.get_protocol_template_info_list(query)
101
+ return self._find_template(self._protocols, name, active, version, first_match)
102
+
103
+ @staticmethod
104
+ def _find_template(templates: list[ElnTemplate] | list[ProtocolTemplateInfo], name: str, active: bool,
105
+ version: int, first_match: bool) -> ElnTemplate | ProtocolTemplateInfo:
106
+ """
107
+ Find the experiment or protocol template with the given information.
108
+ """
109
+ matches = []
110
+ for template in templates:
111
+ if template.template_name != name:
112
+ continue
113
+ if template.active != active:
114
+ continue
115
+ if version is not None and template.template_version != version:
116
+ continue
117
+ matches.append(template)
118
+ if not matches:
119
+ raise SapioException(f"No template with the name \"{name}\"" +
120
+ ("" if version is None else f" and the version {version}") +
121
+ f" found.")
122
+ if not version:
123
+ return max(matches, key=lambda x: x.template_version)
124
+ if len(matches) > 1 and not first_match:
125
+ raise SapioException(f"Multiple templates with the name \"{name}\" found.")
126
+ return matches[0]
127
+
128
+ def get_predefined_field(self, field_name: str, data_type: ElnBaseDataType) -> AbstractVeloxFieldDefinition:
129
+ """
130
+ Get the predefined field of the given name for the given ELN data type.
131
+
132
+ :param field_name: The name of the field.
133
+ :param data_type: The ELN data type of the field.
134
+ :return: The predefined field of the given name for the given ELN data type.
135
+ """
136
+ return self.get_predefined_fields(data_type)[field_name]
137
+
138
+ def get_predefined_fields(self, data_type: ElnBaseDataType) -> dict[str, AbstractVeloxFieldDefinition]:
139
+ """
140
+ Get the predefined fields for the given ELN data type.
141
+
142
+ :param data_type: The ELN data type to get the predefined fields for.
143
+ :return: A dictionary of field name to field definition for the given ELN data type.
144
+ """
145
+ if data_type.data_type_name not in self._predefined_fields:
146
+ fields: list[AbstractVeloxFieldDefinition] = self.eln_man.get_predefined_fields(data_type)
147
+ self._predefined_fields[data_type.data_type_name] = {x.data_field_name: x for x in fields}
148
+ return self._predefined_fields[data_type.data_type_name]
149
+
150
+ def get_field_set(self, name: str) -> ElnFieldSetInfo:
151
+ """
152
+ Get the field set with the given name.
153
+
154
+ :param name: The name of the field set.
155
+ :return: The field set with the given name.
156
+ """
157
+ if not hasattr(self, "_field_sets"):
158
+ self._field_sets = {x.field_set_name: x for x in self.eln_man.get_field_set_info_list()}
159
+ return self._field_sets[name]
160
+
161
+ def get_field_set_fields(self, field_set: ElnFieldSetInfo | int) -> list[AbstractVeloxFieldDefinition]:
162
+ """
163
+ Get the fields of the given field set.
164
+
165
+ :param field_set: The field set to get the fields from. Can be either an ElnFieldSetInfo object or an integer
166
+ for the set ID.
167
+ :return: The fields of the given field set.
168
+ """
169
+ field_set: int = field_set if isinstance(field_set, int) else field_set.field_set_id
170
+ if field_set in self._field_set_fields:
171
+ return self._field_set_fields[field_set]
172
+ self._field_set_fields[field_set] = self.eln_man.get_predefined_fields_from_field_set_id(field_set)
173
+ return self._field_set_fields[field_set]