sapiopycommons 2025.4.30a503__py3-none-any.whl → 2025.5.6a510__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.
- sapiopycommons/callbacks/callback_util.py +116 -64
- sapiopycommons/callbacks/field_builder.py +2 -0
- sapiopycommons/customreport/auto_pagers.py +2 -1
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/pseudo_data_types.py +349 -326
- sapiopycommons/eln/experiment_cache.py +188 -0
- sapiopycommons/eln/experiment_handler.py +336 -719
- sapiopycommons/eln/experiment_step_factory.py +476 -0
- sapiopycommons/eln/plate_designer.py +7 -2
- sapiopycommons/eln/step_creation.py +236 -0
- sapiopycommons/files/file_util.py +4 -4
- sapiopycommons/general/accession_service.py +2 -2
- sapiopycommons/general/aliases.py +4 -1
- sapiopycommons/general/data_structure_util.py +115 -0
- sapiopycommons/general/sapio_links.py +4 -12
- sapiopycommons/processtracking/custom_workflow_handler.py +2 -1
- sapiopycommons/recordmodel/record_handler.py +357 -27
- sapiopycommons/rules/eln_rule_handler.py +8 -1
- sapiopycommons/rules/on_save_rule_handler.py +8 -1
- sapiopycommons/webhook/webhook_handlers.py +3 -0
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.4.30a503.dist-info → sapiopycommons-2025.5.6a510.dist-info}/METADATA +2 -2
- sapiopycommons-2025.5.6a510.dist-info/RECORD +67 -0
- sapiopycommons/ai/__init__.py +0 -0
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.py +0 -43
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.pyi +0 -31
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.py +0 -123
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.pyi +0 -598
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/plan/proto/step_output_pb2.py +0 -45
- sapiopycommons/ai/api/plan/proto/step_output_pb2.pyi +0 -42
- sapiopycommons/ai/api/plan/proto/step_output_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/plan/proto/step_pb2.py +0 -43
- sapiopycommons/ai/api/plan/proto/step_pb2.pyi +0 -43
- sapiopycommons/ai/api/plan/proto/step_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/plan/script/proto/script_pb2.py +0 -53
- sapiopycommons/ai/api/plan/script/proto/script_pb2.pyi +0 -99
- sapiopycommons/ai/api/plan/script/proto/script_pb2_grpc.py +0 -153
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2.py +0 -57
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2.pyi +0 -96
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2.py +0 -67
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2.pyi +0 -220
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2_grpc.py +0 -154
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.py +0 -39
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.pyi +0 -32
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2_grpc.py +0 -24
- sapiopycommons/ai/protobuf_utils.py +0 -454
- sapiopycommons/ai/tool_service_base.py +0 -682
- sapiopycommons/general/html_formatter.py +0 -456
- sapiopycommons-2025.4.30a503.dist-info/RECORD +0 -91
- {sapiopycommons-2025.4.30a503.dist-info → sapiopycommons-2025.5.6a510.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.4.30a503.dist-info → sapiopycommons-2025.5.6a510.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
|
|
7
|
+
from typing import Iterable, TypeAlias, Any, Callable, Container, Collection
|
|
8
8
|
from weakref import WeakValueDictionary
|
|
9
9
|
|
|
10
10
|
from requests import ReadTimeout
|
|
@@ -38,11 +38,18 @@ 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
|
|
44
45
|
|
|
45
46
|
|
|
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.)
|
|
46
53
|
class CallbackUtil:
|
|
47
54
|
user: SapioUser
|
|
48
55
|
callback: ClientCallback
|
|
@@ -193,7 +200,7 @@ class CallbackUtil:
|
|
|
193
200
|
def ok_cancel_dialog(self, title: str, msg: str, default_ok: bool = True) -> bool:
|
|
194
201
|
"""
|
|
195
202
|
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
|
|
203
|
+
dialog using the X in the top right corner.
|
|
197
204
|
|
|
198
205
|
:param title: The title of the dialog.
|
|
199
206
|
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
@@ -205,7 +212,7 @@ class CallbackUtil:
|
|
|
205
212
|
def yes_no_dialog(self, title: str, msg: str, default_yes: bool = True) -> bool:
|
|
206
213
|
"""
|
|
207
214
|
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
|
|
215
|
+
dialog using the X in the top right corner.
|
|
209
216
|
|
|
210
217
|
:param title: The title of the dialog.
|
|
211
218
|
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
@@ -258,7 +265,7 @@ class CallbackUtil:
|
|
|
258
265
|
def form_dialog(self,
|
|
259
266
|
title: str,
|
|
260
267
|
msg: str,
|
|
261
|
-
fields:
|
|
268
|
+
fields: Iterable[AbstractVeloxFieldDefinition],
|
|
262
269
|
values: FieldMap = None,
|
|
263
270
|
column_positions: dict[str, tuple[int, int]] = None,
|
|
264
271
|
*,
|
|
@@ -296,7 +303,7 @@ class CallbackUtil:
|
|
|
296
303
|
def record_form_dialog(self,
|
|
297
304
|
title: str,
|
|
298
305
|
msg: str,
|
|
299
|
-
fields:
|
|
306
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
300
307
|
record: SapioRecord,
|
|
301
308
|
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
302
309
|
editable=None,
|
|
@@ -369,7 +376,7 @@ class CallbackUtil:
|
|
|
369
376
|
def set_record_form_dialog(self,
|
|
370
377
|
title: str,
|
|
371
378
|
msg: str,
|
|
372
|
-
fields:
|
|
379
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
373
380
|
record: SapioRecord,
|
|
374
381
|
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
375
382
|
*,
|
|
@@ -412,7 +419,7 @@ class CallbackUtil:
|
|
|
412
419
|
def create_record_form_dialog(self,
|
|
413
420
|
title: str,
|
|
414
421
|
msg: str,
|
|
415
|
-
fields:
|
|
422
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
416
423
|
wrapper_type: type[WrappedType] | str,
|
|
417
424
|
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
418
425
|
*,
|
|
@@ -618,14 +625,14 @@ class CallbackUtil:
|
|
|
618
625
|
def table_dialog(self,
|
|
619
626
|
title: str,
|
|
620
627
|
msg: str,
|
|
621
|
-
fields:
|
|
622
|
-
values:
|
|
628
|
+
fields: Iterable[AbstractVeloxFieldDefinition],
|
|
629
|
+
values: Iterable[FieldMap],
|
|
623
630
|
*,
|
|
624
631
|
data_type: DataTypeIdentifier = "Default",
|
|
625
632
|
display_name: str | None = None,
|
|
626
633
|
plural_display_name: str | None = None,
|
|
627
634
|
group_by: FieldIdentifier | None = None,
|
|
628
|
-
image_data:
|
|
635
|
+
image_data: Iterable[bytes] | None = None) -> list[FieldMap]:
|
|
629
636
|
"""
|
|
630
637
|
Create a table dialog where the user may input data into the fields of the table. Requires that the caller
|
|
631
638
|
provide the definitions of every field in the table.
|
|
@@ -660,7 +667,7 @@ class CallbackUtil:
|
|
|
660
667
|
temp_dt.record_image_assignable = bool(image_data)
|
|
661
668
|
|
|
662
669
|
# Send the request to the user.
|
|
663
|
-
request = TableEntryDialogRequest(title, msg, temp_dt, values,
|
|
670
|
+
request = TableEntryDialogRequest(title, msg, temp_dt, list(values),
|
|
664
671
|
record_image_data_list=image_data, group_by_field=group_by,
|
|
665
672
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
666
673
|
response: list[FieldMap] = self.__handle_dialog_request(request, self.callback.show_table_entry_dialog)
|
|
@@ -669,14 +676,15 @@ class CallbackUtil:
|
|
|
669
676
|
def record_table_dialog(self,
|
|
670
677
|
title: str,
|
|
671
678
|
msg: str,
|
|
672
|
-
fields:
|
|
673
|
-
records:
|
|
679
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
680
|
+
records: Iterable[SapioRecord],
|
|
674
681
|
editable=None,
|
|
675
682
|
*,
|
|
676
683
|
default_modifier: FieldModifier | None = None,
|
|
677
684
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
678
685
|
group_by: FieldIdentifier | None = None,
|
|
679
|
-
image_data:
|
|
686
|
+
image_data: Iterable[bytes] | None = None,
|
|
687
|
+
index_field: str | None = None) -> list[FieldMap]:
|
|
680
688
|
"""
|
|
681
689
|
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
682
690
|
a given list of records of a singular type.
|
|
@@ -706,6 +714,12 @@ class CallbackUtil:
|
|
|
706
714
|
The user may remove this grouping if they want to.
|
|
707
715
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
708
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.
|
|
709
723
|
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
710
724
|
value from the user for that field for each row.
|
|
711
725
|
"""
|
|
@@ -717,7 +731,10 @@ class CallbackUtil:
|
|
|
717
731
|
if not records:
|
|
718
732
|
raise SapioException("No records provided.")
|
|
719
733
|
data_type: str = AliasUtil.to_singular_data_type_name(records)
|
|
720
|
-
|
|
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)
|
|
721
738
|
|
|
722
739
|
# Convert the group_by parameter to a field name.
|
|
723
740
|
if group_by is not None:
|
|
@@ -750,13 +767,13 @@ class CallbackUtil:
|
|
|
750
767
|
def set_record_table_dialog(self,
|
|
751
768
|
title: str,
|
|
752
769
|
msg: str,
|
|
753
|
-
fields:
|
|
754
|
-
records:
|
|
770
|
+
fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
|
|
771
|
+
records: Iterable[SapioRecord],
|
|
755
772
|
*,
|
|
756
773
|
default_modifier: FieldModifier | None = None,
|
|
757
774
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
758
775
|
group_by: FieldIdentifier | None = None,
|
|
759
|
-
image_data:
|
|
776
|
+
image_data: Iterable[bytes] | None = None):
|
|
760
777
|
"""
|
|
761
778
|
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
762
779
|
a given list of records of a singular type. After the user submits this dialog, the values that the user
|
|
@@ -787,25 +804,30 @@ class CallbackUtil:
|
|
|
787
804
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
788
805
|
the image data list corresponds to the element at the same index in the records list.
|
|
789
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()}"
|
|
790
810
|
results: list[FieldMap] = self.record_table_dialog(title, msg, fields, records,
|
|
791
811
|
default_modifier=default_modifier,
|
|
792
812
|
field_modifiers=field_modifiers,
|
|
793
|
-
group_by=group_by, image_data=image_data
|
|
813
|
+
group_by=group_by, image_data=image_data,
|
|
814
|
+
index_field=index_field)
|
|
794
815
|
records_by_id: dict[int, SapioRecord] = self.rec_handler.map_by_id(records)
|
|
795
816
|
for result in results:
|
|
796
|
-
|
|
817
|
+
index: int = result.pop(index_field)
|
|
818
|
+
records_by_id[index].set_field_values(result)
|
|
797
819
|
|
|
798
820
|
def create_record_table_dialog(self,
|
|
799
821
|
title: str,
|
|
800
822
|
msg: str,
|
|
801
|
-
fields:
|
|
823
|
+
fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
|
|
802
824
|
wrapper_type: type[WrappedType] | str,
|
|
803
825
|
count: int | tuple[int, int],
|
|
804
826
|
*,
|
|
805
827
|
default_modifier: FieldModifier | None = None,
|
|
806
828
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
807
829
|
group_by: FieldIdentifier | None = None,
|
|
808
|
-
image_data:
|
|
830
|
+
image_data: Iterable[bytes] | None = None,
|
|
809
831
|
require_input: bool = False,
|
|
810
832
|
repeat_message: str | None = "Please provide a value to continue.") \
|
|
811
833
|
-> list[WrappedType] | list[PyRecordModel]:
|
|
@@ -860,14 +882,15 @@ class CallbackUtil:
|
|
|
860
882
|
def record_adaptive_dialog(self,
|
|
861
883
|
title: str,
|
|
862
884
|
msg: str,
|
|
863
|
-
fields:
|
|
864
|
-
records:
|
|
885
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
886
|
+
records: Collection[SapioRecord],
|
|
865
887
|
*,
|
|
866
888
|
default_modifier: FieldModifier | None = None,
|
|
867
889
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
868
890
|
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
869
891
|
group_by: FieldIdentifier | None = None,
|
|
870
|
-
image_data:
|
|
892
|
+
image_data: Iterable[bytes] | None = None,
|
|
893
|
+
index_field: str | None = None) -> list[FieldMap]:
|
|
871
894
|
"""
|
|
872
895
|
Create a dialog where the user may input data into the specified fields. The dialog is constructed from
|
|
873
896
|
a given list of records of a singular type.
|
|
@@ -903,6 +926,12 @@ class CallbackUtil:
|
|
|
903
926
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
904
927
|
the image data list corresponds to the element at the same index in the records list. Only used if the
|
|
905
928
|
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.
|
|
906
935
|
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
907
936
|
value from the user for that field for each row. Even if a form was displayed, the field values will still
|
|
908
937
|
be returned in a list.
|
|
@@ -911,23 +940,23 @@ class CallbackUtil:
|
|
|
911
940
|
if not count:
|
|
912
941
|
raise SapioException("No records provided.")
|
|
913
942
|
if count == 1:
|
|
914
|
-
return [self.record_form_dialog(title, msg, fields, records[0], column_positions,
|
|
943
|
+
return [self.record_form_dialog(title, msg, fields, list(records)[0], column_positions,
|
|
915
944
|
default_modifier=default_modifier, field_modifiers=field_modifiers)]
|
|
916
945
|
return self.record_table_dialog(title, msg, fields, records,
|
|
917
946
|
default_modifier=default_modifier, field_modifiers=field_modifiers,
|
|
918
|
-
group_by=group_by, image_data=image_data)
|
|
947
|
+
group_by=group_by, image_data=image_data, index_field=index_field)
|
|
919
948
|
|
|
920
949
|
def set_record_adaptive_dialog(self,
|
|
921
950
|
title: str,
|
|
922
951
|
msg: str,
|
|
923
|
-
fields:
|
|
924
|
-
records:
|
|
952
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
953
|
+
records: Collection[SapioRecord],
|
|
925
954
|
*,
|
|
926
955
|
default_modifier: FieldModifier | None = None,
|
|
927
956
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
928
957
|
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
929
958
|
group_by: FieldIdentifier | None = None,
|
|
930
|
-
image_data:
|
|
959
|
+
image_data: Iterable[bytes] | None = None) -> None:
|
|
931
960
|
"""
|
|
932
961
|
Create a dialog where the user may input data into the fields of the dialog. The dialog is constructed from
|
|
933
962
|
a given list of records of a singular type. After the user submits this dialog, the values that the user
|
|
@@ -969,7 +998,7 @@ class CallbackUtil:
|
|
|
969
998
|
if not count:
|
|
970
999
|
raise SapioException("No records provided.")
|
|
971
1000
|
if count == 1:
|
|
972
|
-
self.set_record_form_dialog(title, msg, fields, records[0], column_positions,
|
|
1001
|
+
self.set_record_form_dialog(title, msg, fields, list(records)[0], column_positions,
|
|
973
1002
|
default_modifier=default_modifier, field_modifiers=field_modifiers)
|
|
974
1003
|
else:
|
|
975
1004
|
self.set_record_table_dialog(title, msg, fields, records,
|
|
@@ -979,7 +1008,7 @@ class CallbackUtil:
|
|
|
979
1008
|
def create_record_adaptive_dialog(self,
|
|
980
1009
|
title: str,
|
|
981
1010
|
msg: str,
|
|
982
|
-
fields:
|
|
1011
|
+
fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
|
|
983
1012
|
wrapper_type: type[WrappedType] | str,
|
|
984
1013
|
count: int | tuple[int, int],
|
|
985
1014
|
*,
|
|
@@ -987,7 +1016,7 @@ class CallbackUtil:
|
|
|
987
1016
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
988
1017
|
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
989
1018
|
group_by: FieldIdentifier | None = None,
|
|
990
|
-
image_data:
|
|
1019
|
+
image_data: Iterable[bytes] | None = None,
|
|
991
1020
|
require_input: bool = False,
|
|
992
1021
|
repeat_message: str | None = "Please provide a value to continue.") \
|
|
993
1022
|
-> list[WrappedType]:
|
|
@@ -1050,8 +1079,8 @@ class CallbackUtil:
|
|
|
1050
1079
|
def multi_type_table_dialog(self,
|
|
1051
1080
|
title: str,
|
|
1052
1081
|
msg: str,
|
|
1053
|
-
fields:
|
|
1054
|
-
row_contents:
|
|
1082
|
+
fields: Iterable[tuple[DataTypeIdentifier, FieldIdentifier] | AbstractVeloxFieldDefinition],
|
|
1083
|
+
row_contents: Iterable[Iterable[SapioRecord | FieldMap]],
|
|
1055
1084
|
*,
|
|
1056
1085
|
default_modifier: FieldModifier | None = None,
|
|
1057
1086
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
@@ -1187,7 +1216,7 @@ class CallbackUtil:
|
|
|
1187
1216
|
if rec is None:
|
|
1188
1217
|
continue
|
|
1189
1218
|
# Map records to their data type name. Map field maps to Default.
|
|
1190
|
-
dt: str = "Default" if isinstance(rec, dict) else AliasUtil.
|
|
1219
|
+
dt: str = "Default" if isinstance(rec, dict) else AliasUtil.to_data_type_name(rec)
|
|
1191
1220
|
# Warn if the same data type name appears more than once.
|
|
1192
1221
|
if dt in row_records:
|
|
1193
1222
|
raise SapioException(f"The data type \"{dt}\" appears more than once in the given row contents.")
|
|
@@ -1226,7 +1255,7 @@ class CallbackUtil:
|
|
|
1226
1255
|
layout: DataTypeLayoutIdentifier = None,
|
|
1227
1256
|
minimized: bool = False,
|
|
1228
1257
|
access_level: FormAccessLevel | None = None,
|
|
1229
|
-
plugin_path_list:
|
|
1258
|
+
plugin_path_list: Iterable[str] | None = None) -> None:
|
|
1230
1259
|
"""
|
|
1231
1260
|
Create an IDV dialog for the given record. This IDV may use an existing layout already defined in the system,
|
|
1232
1261
|
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 +1292,15 @@ class CallbackUtil:
|
|
|
1263
1292
|
# CR-47326: Allow the selection dialog functions to preselect rows/records in the table.
|
|
1264
1293
|
def selection_dialog(self,
|
|
1265
1294
|
msg: str,
|
|
1266
|
-
fields:
|
|
1267
|
-
values:
|
|
1295
|
+
fields: Iterable[AbstractVeloxFieldDefinition],
|
|
1296
|
+
values: Iterable[FieldMap],
|
|
1268
1297
|
multi_select: bool = True,
|
|
1269
|
-
preselected_rows:
|
|
1298
|
+
preselected_rows: Iterable[FieldMap | RecordIdentifier] | None = None,
|
|
1270
1299
|
*,
|
|
1271
1300
|
data_type: DataTypeIdentifier = "Default",
|
|
1272
1301
|
display_name: str | None = None,
|
|
1273
1302
|
plural_display_name: str | None = None,
|
|
1274
|
-
image_data:
|
|
1303
|
+
image_data: Iterable[bytes] | None = None,
|
|
1275
1304
|
require_selection: bool = False,
|
|
1276
1305
|
repeat_message: str | None = "Please provide a selection to continue.") -> list[FieldMap]:
|
|
1277
1306
|
"""
|
|
@@ -1329,6 +1358,7 @@ class CallbackUtil:
|
|
|
1329
1358
|
|
|
1330
1359
|
# Add a RecordId definition to the fields if one is not already present. This is necessary for the
|
|
1331
1360
|
# pre-selected records parameter to function.
|
|
1361
|
+
fields = list(fields)
|
|
1332
1362
|
if "RecordId" not in [x.data_field_name for x in fields]:
|
|
1333
1363
|
builder = FieldBuilder(data_type)
|
|
1334
1364
|
fields.append(builder.long_field("RecordId", abstract_info=AnyFieldInfo(visible=False)))
|
|
@@ -1338,7 +1368,7 @@ class CallbackUtil:
|
|
|
1338
1368
|
temp_dt.record_image_assignable = bool(image_data)
|
|
1339
1369
|
|
|
1340
1370
|
# Send the request to the user.
|
|
1341
|
-
request = TempTableSelectionRequest(temp_dt, msg, values, image_data, preselected_rows, multi_select)
|
|
1371
|
+
request = TempTableSelectionRequest(temp_dt, msg, list(values), image_data, preselected_rows, multi_select)
|
|
1342
1372
|
# If require_selection is true, repeat the request if the user didn't make a selection.
|
|
1343
1373
|
while True:
|
|
1344
1374
|
response: list[FieldMap] = self.__handle_dialog_request(request,
|
|
@@ -1351,12 +1381,12 @@ class CallbackUtil:
|
|
|
1351
1381
|
|
|
1352
1382
|
def record_selection_dialog(self,
|
|
1353
1383
|
msg: str,
|
|
1354
|
-
fields:
|
|
1355
|
-
records:
|
|
1384
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
1385
|
+
records: Iterable[SapioRecord],
|
|
1356
1386
|
multi_select: bool = True,
|
|
1357
|
-
preselected_records:
|
|
1387
|
+
preselected_records: Iterable[RecordIdentifier] | None = None,
|
|
1358
1388
|
*,
|
|
1359
|
-
image_data:
|
|
1389
|
+
image_data: Iterable[bytes] | None = None,
|
|
1360
1390
|
require_selection: bool = False,
|
|
1361
1391
|
repeat_message: str | None = "Please provide a selection to continue.") \
|
|
1362
1392
|
-> list[SapioRecord]:
|
|
@@ -1438,12 +1468,12 @@ class CallbackUtil:
|
|
|
1438
1468
|
msg: str,
|
|
1439
1469
|
multi_select: bool = True,
|
|
1440
1470
|
only_key_fields: bool = False,
|
|
1441
|
-
search_types:
|
|
1471
|
+
search_types: Iterable[SearchType] | None = None,
|
|
1442
1472
|
scan_criteria: ScanToSelectCriteria | None = None,
|
|
1443
1473
|
custom_search: CustomReport | CustomReportCriteria | str | None = None,
|
|
1444
|
-
preselected_records:
|
|
1445
|
-
record_blacklist:
|
|
1446
|
-
record_whitelist:
|
|
1474
|
+
preselected_records: Iterable[RecordIdentifier] | None = None,
|
|
1475
|
+
record_blacklist: Iterable[RecordIdentifier] | None = None,
|
|
1476
|
+
record_whitelist: Iterable[RecordIdentifier] | None = None,
|
|
1447
1477
|
allow_creation: bool = False,
|
|
1448
1478
|
default_creation_number: int = 1,
|
|
1449
1479
|
*,
|
|
@@ -1533,7 +1563,7 @@ class CallbackUtil:
|
|
|
1533
1563
|
title: str,
|
|
1534
1564
|
msg: str,
|
|
1535
1565
|
show_comment: bool = True,
|
|
1536
|
-
additional_fields:
|
|
1566
|
+
additional_fields: Iterable[AbstractVeloxFieldDefinition] | None = None,
|
|
1537
1567
|
*,
|
|
1538
1568
|
require_authentication: bool = False) -> ESigningResponsePojo:
|
|
1539
1569
|
"""
|
|
@@ -1572,7 +1602,7 @@ class CallbackUtil:
|
|
|
1572
1602
|
popup_type=PopupType.Error)
|
|
1573
1603
|
return response
|
|
1574
1604
|
|
|
1575
|
-
def request_file(self, title: str, exts:
|
|
1605
|
+
def request_file(self, title: str, exts: Iterable[str] | None = None,
|
|
1576
1606
|
show_image_editor: bool = False, show_camera_button: bool = False) -> tuple[str, bytes]:
|
|
1577
1607
|
"""
|
|
1578
1608
|
Request a single file from the user.
|
|
@@ -1605,7 +1635,7 @@ class CallbackUtil:
|
|
|
1605
1635
|
self.__verify_file(file_path, sink.data, exts)
|
|
1606
1636
|
return file_path, sink.data
|
|
1607
1637
|
|
|
1608
|
-
def request_files(self, title: str, exts:
|
|
1638
|
+
def request_files(self, title: str, exts: Iterable[str] | None = None,
|
|
1609
1639
|
show_image_editor: bool = False, show_camera_button: bool = False) -> dict[str, bytes]:
|
|
1610
1640
|
"""
|
|
1611
1641
|
Request multiple files from the user.
|
|
@@ -1637,7 +1667,7 @@ class CallbackUtil:
|
|
|
1637
1667
|
return ret_dict
|
|
1638
1668
|
|
|
1639
1669
|
@staticmethod
|
|
1640
|
-
def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions:
|
|
1670
|
+
def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: Iterable[str]) -> None:
|
|
1641
1671
|
"""
|
|
1642
1672
|
Verify that the provided file was read (i.e. the file path and file bytes aren't None or empty) and that it
|
|
1643
1673
|
has the correct file extension. Raises a user error exception if something about the file is incorrect.
|
|
@@ -1648,7 +1678,7 @@ class CallbackUtil:
|
|
|
1648
1678
|
"""
|
|
1649
1679
|
if file_path is None or len(file_path) == 0 or file_bytes is None or len(file_bytes) == 0:
|
|
1650
1680
|
raise SapioUserErrorException("Empty file provided or file unable to be read.")
|
|
1651
|
-
if
|
|
1681
|
+
if allowed_extensions:
|
|
1652
1682
|
matches: bool = False
|
|
1653
1683
|
for ext in allowed_extensions:
|
|
1654
1684
|
if file_path.endswith("." + ext.lstrip(".")):
|
|
@@ -1665,8 +1695,8 @@ class CallbackUtil:
|
|
|
1665
1695
|
:param file_name: The name of the file.
|
|
1666
1696
|
:param file_data: The data of the file, provided as either a string or as a bytes array.
|
|
1667
1697
|
"""
|
|
1668
|
-
|
|
1669
|
-
|
|
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)
|
|
1670
1700
|
|
|
1671
1701
|
def write_zip_file(self, zip_name: str, files: dict[str, str | bytes]) -> None:
|
|
1672
1702
|
"""
|
|
@@ -1675,12 +1705,33 @@ class CallbackUtil:
|
|
|
1675
1705
|
:param zip_name: The name of the zip file.
|
|
1676
1706
|
:param files: A dictionary of the files to add to the zip file.
|
|
1677
1707
|
"""
|
|
1678
|
-
|
|
1679
|
-
|
|
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
|
|
1680
1731
|
|
|
1681
1732
|
@staticmethod
|
|
1682
1733
|
def __temp_dt_from_field_defs(data_type: DataTypeIdentifier, display_name: str | None,
|
|
1683
|
-
plural_display_name: str | None, fields:
|
|
1734
|
+
plural_display_name: str | None, fields: Iterable[AbstractVeloxFieldDefinition],
|
|
1684
1735
|
column_positions: dict[str, tuple[int, int]] | None) -> TemporaryDataType:
|
|
1685
1736
|
"""
|
|
1686
1737
|
Construct a Temporary Data Type definition from a provided list of field definitions for use in a callback.
|
|
@@ -1714,7 +1765,7 @@ class CallbackUtil:
|
|
|
1714
1765
|
builder.add_field(field_def, column, span)
|
|
1715
1766
|
return builder.get_temporary_data_type()
|
|
1716
1767
|
|
|
1717
|
-
def __temp_dt_from_field_names(self, data_type: str, fields:
|
|
1768
|
+
def __temp_dt_from_field_names(self, data_type: str, fields: Iterable[FieldIdentifier | FieldFilterCriteria],
|
|
1718
1769
|
column_positions: dict[str, tuple[int, int]] | None,
|
|
1719
1770
|
default_modifier: FieldModifier, field_modifiers: dict[str, FieldModifier]) \
|
|
1720
1771
|
-> TemporaryDataType:
|
|
@@ -1731,6 +1782,7 @@ class CallbackUtil:
|
|
|
1731
1782
|
|
|
1732
1783
|
# Determine if any FieldFilterCriteria were provided. If so, remove them from the fields list so that it
|
|
1733
1784
|
# contains only field identifiers.
|
|
1785
|
+
fields = list(fields)
|
|
1734
1786
|
filter_criteria: list[FieldFilterCriteria] = [x for x in fields if isinstance(x, FieldFilterCriteria)]
|
|
1735
1787
|
for criteria in filter_criteria:
|
|
1736
1788
|
fields.remove(criteria)
|
|
@@ -1983,15 +2035,15 @@ class FieldFilterCriteria:
|
|
|
1983
2035
|
key_field: bool | None
|
|
1984
2036
|
identifier: bool | None
|
|
1985
2037
|
system_field: bool | None
|
|
1986
|
-
field_types:
|
|
1987
|
-
not_field_types:
|
|
2038
|
+
field_types: Container[FieldType] | None
|
|
2039
|
+
not_field_types: Container[FieldType] | None
|
|
1988
2040
|
matches_tag: str | None
|
|
1989
2041
|
contains_tag: str | None
|
|
1990
2042
|
regex_tag: str | re.Pattern[str] | None
|
|
1991
2043
|
|
|
1992
2044
|
def __init__(self, *, required: bool | None = None, editable: bool | None = None, key_field: bool | None = None,
|
|
1993
2045
|
identifier: bool | None = None, system_field: bool | None = None,
|
|
1994
|
-
field_types:
|
|
2046
|
+
field_types: Container[FieldType] | None = None, not_field_types: Container[FieldType] | None = None,
|
|
1995
2047
|
matches_tag: str | None = None, contains_tag: str | None = None,
|
|
1996
2048
|
regex_tag: str | re.Pattern[str] | None = None):
|
|
1997
2049
|
"""
|
|
@@ -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:
|
|
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
|
|
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),
|