sapiopycommons 2024.11.8a355__py3-none-any.whl → 2024.11.9a360__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 (47) hide show
  1. sapiopycommons/callbacks/callback_util.py +83 -532
  2. sapiopycommons/chem/IndigoMolecules.py +0 -2
  3. sapiopycommons/chem/Molecules.py +18 -77
  4. sapiopycommons/datatype/attachment_util.py +10 -11
  5. sapiopycommons/eln/experiment_handler.py +70 -272
  6. sapiopycommons/files/complex_data_loader.py +4 -5
  7. sapiopycommons/files/file_bridge.py +24 -31
  8. sapiopycommons/files/file_data_handler.py +5 -2
  9. sapiopycommons/files/file_util.py +9 -59
  10. sapiopycommons/files/file_validator.py +6 -92
  11. sapiopycommons/files/file_writer.py +15 -44
  12. sapiopycommons/general/aliases.py +6 -207
  13. sapiopycommons/general/custom_report_util.py +37 -212
  14. sapiopycommons/general/exceptions.py +8 -21
  15. sapiopycommons/general/popup_util.py +0 -21
  16. sapiopycommons/general/time_util.py +2 -8
  17. sapiopycommons/processtracking/endpoints.py +22 -22
  18. sapiopycommons/recordmodel/record_handler.py +97 -481
  19. sapiopycommons/rules/eln_rule_handler.py +25 -34
  20. sapiopycommons/rules/on_save_rule_handler.py +31 -34
  21. sapiopycommons/webhook/webhook_handlers.py +42 -201
  22. {sapiopycommons-2024.11.8a355.dist-info → sapiopycommons-2024.11.9a360.dist-info}/METADATA +2 -4
  23. sapiopycommons-2024.11.9a360.dist-info/RECORD +38 -0
  24. sapiopycommons/callbacks/field_builder.py +0 -537
  25. sapiopycommons/customreport/__init__.py +0 -0
  26. sapiopycommons/customreport/column_builder.py +0 -60
  27. sapiopycommons/customreport/custom_report_builder.py +0 -130
  28. sapiopycommons/customreport/term_builder.py +0 -299
  29. sapiopycommons/datatype/data_fields.py +0 -61
  30. sapiopycommons/datatype/pseudo_data_types.py +0 -440
  31. sapiopycommons/eln/experiment_report_util.py +0 -653
  32. sapiopycommons/files/file_bridge_handler.py +0 -340
  33. sapiopycommons/flowcyto/flow_cyto.py +0 -77
  34. sapiopycommons/flowcyto/flowcyto_data.py +0 -75
  35. sapiopycommons/general/accession_service.py +0 -375
  36. sapiopycommons/general/audit_log.py +0 -189
  37. sapiopycommons/general/sapio_links.py +0 -50
  38. sapiopycommons/multimodal/multimodal.py +0 -146
  39. sapiopycommons/multimodal/multimodal_data.py +0 -489
  40. sapiopycommons/processtracking/custom_workflow_handler.py +0 -406
  41. sapiopycommons/sftpconnect/__init__.py +0 -0
  42. sapiopycommons/sftpconnect/sftp_builder.py +0 -69
  43. sapiopycommons/webhook/webhook_context.py +0 -39
  44. sapiopycommons/webhook/webservice_handlers.py +0 -67
  45. sapiopycommons-2024.11.8a355.dist-info/RECORD +0 -59
  46. {sapiopycommons-2024.11.8a355.dist-info → sapiopycommons-2024.11.9a360.dist-info}/WHEEL +0 -0
  47. {sapiopycommons-2024.11.8a355.dist-info → sapiopycommons-2024.11.9a360.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,6 @@
1
- from __future__ import annotations
2
-
3
1
  import io
4
- from weakref import WeakValueDictionary
2
+ from typing import Any
5
3
 
6
- from requests import ReadTimeout
7
4
  from sapiopylib.rest.ClientCallbackService import ClientCallback
8
5
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
9
6
  from sapiopylib.rest.User import SapioUser
@@ -12,24 +9,22 @@ from sapiopylib.rest.pojo.DataRecord import DataRecord
12
9
  from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
13
10
  from sapiopylib.rest.pojo.datatype.DataTypeLayout import DataTypeLayout
14
11
  from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition, VeloxStringFieldDefinition, \
15
- VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition, FieldDefinitionParser
12
+ VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition
16
13
  from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import OptionDialogRequest, ListDialogRequest, \
17
14
  FormEntryDialogRequest, InputDialogCriteria, TableEntryDialogRequest, ESigningRequestPojo, \
18
- DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, MultiFilePromptRequest, \
19
- TempTableSelectionRequest, DisplayPopupRequest, PopupType
15
+ DataRecordSelectionRequest, DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, \
16
+ MultiFilePromptRequest
20
17
  from sapiopylib.rest.pojo.webhook.ClientCallbackResult import ESigningResponsePojo
18
+ from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
21
19
  from sapiopylib.rest.pojo.webhook.WebhookEnums import FormAccessLevel, ScanToSelectCriteria, SearchType
22
20
  from sapiopylib.rest.utils.DataTypeCacheManager import DataTypeCacheManager
23
21
  from sapiopylib.rest.utils.FormBuilder import FormBuilder
24
22
  from sapiopylib.rest.utils.recorddatasinks import InMemoryRecordDataSink
25
23
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
26
24
 
27
- from sapiopycommons.files.file_util import FileUtil
28
- from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier, FieldValue, \
29
- UserIdentifier
25
+ from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier
30
26
  from sapiopycommons.general.custom_report_util import CustomReportUtil
31
- from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException, \
32
- SapioDialogTimeoutException
27
+ from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException
33
28
  from sapiopycommons.recordmodel.record_handler import RecordHandler
34
29
 
35
30
 
@@ -37,104 +32,29 @@ class CallbackUtil:
37
32
  user: SapioUser
38
33
  callback: ClientCallback
39
34
  dt_cache: DataTypeCacheManager
40
- _original_timeout: int
41
- timeout_seconds: int
42
35
  width_pixels: int | None
43
36
  width_percent: float | None
44
37
 
45
- __instances: WeakValueDictionary[SapioUser, CallbackUtil] = WeakValueDictionary()
46
- __initialized: bool
47
-
48
- def __new__(cls, context: UserIdentifier):
49
- """
50
- :param context: The current webhook context or a user object to send requests from.
51
- """
52
- user = AliasUtil.to_sapio_user(context)
53
- obj = cls.__instances.get(user)
54
- if not obj:
55
- obj = object.__new__(cls)
56
- obj.__initialized = False
57
- cls.__instances[user] = obj
58
- return obj
59
-
60
- def __init__(self, context: UserIdentifier):
38
+ def __init__(self, context: SapioWebhookContext | SapioUser):
61
39
  """
62
40
  :param context: The current webhook context or a user object to send requests from.
63
41
  """
64
- if self.__initialized:
65
- return
66
- self.__initialized = True
67
-
68
- self.user = AliasUtil.to_sapio_user(context)
42
+ self.user = context if isinstance(context, SapioUser) else context.user
69
43
  self.callback = DataMgmtServer.get_client_callback(self.user)
70
44
  self.dt_cache = DataTypeCacheManager(self.user)
71
- self._original_timeout = self.user.timeout_seconds
72
- self.timeout_seconds = self.user.timeout_seconds
73
45
  self.width_pixels = None
74
46
  self.width_percent = None
75
47
 
76
- def set_dialog_width(self, width_pixels: int | None = None, width_percent: float | None = None):
48
+ def set_dialog_width(self, width_pixels: int | None, width_percent: float | None):
77
49
  """
78
50
  Set the width that dialogs will appear as for those dialogs that support specifying their width.
79
51
 
80
52
  :param width_pixels: The number of pixels wide that dialogs will appear as.
81
- :param width_percent: The percentage (as a value between 0 and 1) of the client's screen width that dialogs
82
- will appear as.
53
+ :param width_percent: The percentage of the client's screen width that dialogs will appear as.
83
54
  """
84
- if width_pixels is not None and width_percent is not None:
85
- raise SapioException("Cannot set both width_pixels and width_percent at once.")
86
55
  self.width_pixels = width_pixels
87
56
  self.width_percent = width_percent
88
-
89
- def set_dialog_timeout(self, timeout: int):
90
- """
91
- Alter the timeout time used for callback requests that create dialogs for the user to interact with. By default,
92
- a CallbackUtil will use the timeout time of the SapioUser provided to it. By altering this, a different timeout
93
- time is used.
94
-
95
- :param timeout: The number of seconds that must elapse before a SapioDialogTimeoutException is thrown by
96
- any callback that creates a dialog for the user to interact with.
97
- """
98
- self.timeout_seconds = timeout
99
-
100
- def toaster_popup(self, message: str, title: str = "", popup_type: PopupType = PopupType.Info) -> None:
101
- """
102
- Display a toaster popup in the bottom right corner of the user's screen.
103
-
104
- :param message: The message to display in the toaster.
105
- :param title: The title to display at the top of the toaster.
106
- :param popup_type: The popup type to use for the toaster. This controls the color that the toaster appears with.
107
- Info is blue, Success is green, Warning is yellow, and Error is red
108
- """
109
- self.callback.display_popup(DisplayPopupRequest(title, message, popup_type))
110
-
111
- def display_info(self, message: str) -> None:
112
- """
113
- Display an info message to the user in a dialog. Repeated calls to this function will append the new messages
114
- to the same dialog if it is still opened by the user.
115
-
116
- :param message: The message to display to the user.
117
- """
118
- self.callback.display_info(message)
119
-
120
- def display_warning(self, message: str) -> None:
121
- """
122
- Display a warning message to the user in a dialog. Repeated calls to this function will append the new messages
123
- to the same dialog if it is still opened by the user.
124
-
125
- :param message: The message to display to the user.
126
- """
127
- self.callback.display_warning(message)
128
-
129
- def display_error(self, message: str) -> None:
130
- """
131
- Display an error message to the user in a dialog. Repeated calls to this function will append the new messages
132
- to the same dialog if it is still opened by the user.
133
-
134
- :param message: The message to display to the user.
135
- """
136
- self.callback.display_error(message)
137
-
57
+
138
58
  def option_dialog(self, title: str, msg: str, options: list[str], default_option: int = 0,
139
59
  user_can_cancel: bool = False) -> str:
140
60
  """
@@ -149,15 +69,8 @@ class CallbackUtil:
149
69
  SapioUserCancelledException is thrown.
150
70
  :return: The name of the button that the user selected.
151
71
  """
152
- request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
153
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
154
- try:
155
- self.user.timeout_seconds = self.timeout_seconds
156
- response: int | None = self.callback.show_option_dialog(request)
157
- except ReadTimeout:
158
- raise SapioDialogTimeoutException()
159
- finally:
160
- self.user.timeout_seconds = self._original_timeout
72
+ request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel)
73
+ response: int | None = self.callback.show_option_dialog(request)
161
74
  if response is None:
162
75
  raise SapioUserCancelledException()
163
76
  return options[response]
@@ -196,27 +109,17 @@ class CallbackUtil:
196
109
  """
197
110
  return self.option_dialog(title, msg, ["Yes", "No"], 0 if default_yes else 1, False) == "Yes"
198
111
 
199
- def list_dialog(self, title: str, options: list[str], multi_select: bool = False,
200
- preselected_values: list[str] | None = None) -> list[str]:
112
+ def list_dialog(self, title: str, options: list[str], multi_select: bool = False) -> list[str]:
201
113
  """
202
114
  Create a list dialog with the given options for the user to choose from.
203
115
 
204
116
  :param title: The title of the dialog.
205
117
  :param options: The list options that the user has to choose from.
206
118
  :param multi_select: Whether the user is able to select multiple options from the list.
207
- :param preselected_values: A list of values that will already be selected when the list dialog is created. The
208
- user can unselect these values if they want to.
209
119
  :return: The list of options that the user selected.
210
120
  """
211
- request = ListDialogRequest(title, multi_select, options, preselected_values,
212
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
213
- try:
214
- self.user.timeout_seconds = self.timeout_seconds
215
- response: list[str] | None = self.callback.show_list_dialog(request)
216
- except ReadTimeout:
217
- raise SapioDialogTimeoutException()
218
- finally:
219
- self.user.timeout_seconds = self._original_timeout
121
+ request = ListDialogRequest(title, multi_select, options)
122
+ response: list[str] | None = self.callback.show_list_dialog(request)
220
123
  if response is None:
221
124
  raise SapioUserCancelledException()
222
125
  return response
@@ -258,6 +161,8 @@ class CallbackUtil:
258
161
  builder = FormBuilder(data_type, display_name, plural_display_name)
259
162
  for field_def in fields:
260
163
  field_name = field_def.data_field_name
164
+ if values and hasattr(field_def, "default_value"):
165
+ field_def.default_value = values.get(field_name)
261
166
  column: int = 0
262
167
  span: int = 4
263
168
  if column_positions and field_name in column_positions:
@@ -266,15 +171,8 @@ class CallbackUtil:
266
171
  span = position[1]
267
172
  builder.add_field(field_def, column, span)
268
173
 
269
- request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
270
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
271
- try:
272
- self.user.timeout_seconds = self.timeout_seconds
273
- response: FieldMap | None = self.callback.show_form_entry_dialog(request)
274
- except ReadTimeout:
275
- raise SapioDialogTimeoutException()
276
- finally:
277
- self.user.timeout_seconds = self._original_timeout
174
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type())
175
+ response: FieldMap | None = self.callback.show_form_entry_dialog(request)
278
176
  if response is None:
279
177
  raise SapioUserCancelledException()
280
178
  return response
@@ -311,39 +209,32 @@ class CallbackUtil:
311
209
  type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
312
210
  field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
313
211
 
314
- # Make everything visible, because presumably the caller gave a field name because they want it to be seen.
315
- modifier = FieldModifier(visible=True, editable=editable)
316
-
317
212
  # Build the form using only those fields that are desired.
318
- values: dict[str, FieldValue] = {}
319
213
  builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
320
214
  for field_name in fields:
321
215
  field_def = field_defs.get(field_name)
322
216
  if field_def is None:
323
217
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
324
- values[field_name] = record.get_field_value(field_name)
218
+ if editable is not None:
219
+ field_def.editable = editable
220
+ field_def.visible = True
221
+ if hasattr(field_def, "default_value"):
222
+ field_def.default_value = record.get_field_value(field_name)
325
223
  column: int = 0
326
224
  span: int = 4
327
225
  if column_positions and field_name in column_positions:
328
226
  position = column_positions.get(field_name)
329
227
  column = position[0]
330
228
  span = position[1]
331
- builder.add_field(modifier.modify_field(field_def), column, span)
332
-
333
- request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
334
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
335
- try:
336
- self.user.timeout_seconds = self.timeout_seconds
337
- response: FieldMap | None = self.callback.show_form_entry_dialog(request)
338
- except ReadTimeout:
339
- raise SapioDialogTimeoutException()
340
- finally:
341
- self.user.timeout_seconds = self._original_timeout
229
+ builder.add_field(field_def, column, span)
230
+
231
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type())
232
+ response: FieldMap | None = self.callback.show_form_entry_dialog(request)
342
233
  if response is None:
343
234
  raise SapioUserCancelledException()
344
235
  return response
345
236
 
346
- def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) -> FieldValue:
237
+ def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) -> Any:
347
238
  """
348
239
  Create an input dialog where the user must input data for a singular field.
349
240
 
@@ -352,15 +243,8 @@ class CallbackUtil:
352
243
  :param field: The definition for a field that the user must provide input to.
353
244
  :return: The response value from the user for the given field.
354
245
  """
355
- request = InputDialogCriteria(title, msg, field,
356
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
357
- try:
358
- self.user.timeout_seconds = self.timeout_seconds
359
- response: FieldValue | None = self.callback.show_input_dialog(request)
360
- except ReadTimeout:
361
- raise SapioDialogTimeoutException()
362
- finally:
363
- self.user.timeout_seconds = self._original_timeout
246
+ request = InputDialogCriteria(title, msg, field, self.width_pixels, self.width_percent)
247
+ response: Any | None = self.callback.show_input_dialog(request)
364
248
  if response is None:
365
249
  raise SapioUserCancelledException()
366
250
  return response
@@ -375,7 +259,7 @@ class CallbackUtil:
375
259
  :param field_name: The name and display name of the string field.
376
260
  :param default_value: The default value to place into the string field, if any.
377
261
  :param max_length: The max length of the string value. If not provided, uses the length of the default value.
378
- If neither this nor a default value are provided, defaults to 100 characters.
262
+ If neither this or a default value are not provided, defaults to 100 characters.
379
263
  :param editable: Whether the field is editable by the user.
380
264
  :param kwargs: Any additional keyword arguments to pass to the field definition.
381
265
  :return: The string that the user input into the dialog.
@@ -436,8 +320,6 @@ class CallbackUtil:
436
320
  msg: str,
437
321
  fields: list[AbstractVeloxFieldDefinition],
438
322
  values: list[FieldMap],
439
- group_by: str | None = None,
440
- image_data: list[bytes] | None = None,
441
323
  *,
442
324
  data_type: str = "Default",
443
325
  display_name: str | None = None,
@@ -451,10 +333,6 @@ class CallbackUtil:
451
333
  :param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
452
334
  they are provided in this list.
453
335
  :param values: The values to set for each row of the table.
454
- :param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
455
- The user may remove this grouping if they want to.
456
- :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
457
- the image data list corresponds to the element at the same index in the values list.
458
336
  :param data_type: The data type name for the temporary data type that will be created for this table.
459
337
  :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
460
338
  name.
@@ -468,24 +346,12 @@ class CallbackUtil:
468
346
  if plural_display_name is None:
469
347
  plural_display_name = display_name + "s"
470
348
 
471
- # Key fields display their columns in order before all non-key fields.
472
- # Unmark key fields so that the column order is respected exactly as the caller provides it.
473
- modifier = FieldModifier(key_field=False)
474
-
475
349
  builder = FormBuilder(data_type, display_name, plural_display_name)
476
- for field in fields:
477
- builder.add_field(modifier.modify_field(field))
478
-
479
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
480
- record_image_data_list=image_data, group_by_field=group_by,
481
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
482
- try:
483
- self.user.timeout_seconds = self.timeout_seconds
484
- response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
485
- except ReadTimeout:
486
- raise SapioDialogTimeoutException()
487
- finally:
488
- self.user.timeout_seconds = self._original_timeout
350
+ for column in fields:
351
+ builder.add_field(column)
352
+
353
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values)
354
+ response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
489
355
  if response is None:
490
356
  raise SapioUserCancelledException()
491
357
  return response
@@ -495,14 +361,11 @@ class CallbackUtil:
495
361
  msg: str,
496
362
  fields: list[str],
497
363
  records: list[SapioRecord],
498
- editable: bool | None = True,
499
- group_by: str | None = None,
500
- image_data: list[bytes] | None = None) -> list[FieldMap]:
364
+ editable: bool | None = True) -> list[FieldMap]:
501
365
  """
502
366
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
503
- a given list of records of a singular type. Provided field names must match fields on the definition of the data
504
- type of the given records. The fields that are displayed will have their default value be that of the fields on
505
- the given records.
367
+ a given list of records. Provided field names must match fields on the definition of the data type of the given
368
+ records. The fields that are displayed will have their default value be that of the fields on the given records.
506
369
 
507
370
  Makes webservice calls to get the data type definition and fields of the given records if they weren't
508
371
  previously cached.
@@ -514,15 +377,9 @@ class CallbackUtil:
514
377
  they are provided in this list.
515
378
  :param editable: If true, all fields are displayed as editable. If false, all fields are displayed as
516
379
  uneditable. If none, only those fields that are defined as editable by the data designer will be editable.
517
- :param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
518
- The user may remove this grouping if they want to.
519
- :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
520
- the image data list corresponds to the element at the same index in the records list.
521
380
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
522
381
  value from the user for that field for each row.
523
382
  """
524
- if not records:
525
- raise SapioException("No records provided.")
526
383
  data_types: set[str] = {x.data_type_name for x in records}
527
384
  if len(data_types) > 1:
528
385
  raise SapioException("Multiple data type names encountered in records list for record table popup.")
@@ -533,193 +390,22 @@ class CallbackUtil:
533
390
  type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
534
391
  field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
535
392
 
536
- # Key fields display their columns in order before all non-key fields.
537
- # Unmark key fields so that the column order is respected exactly as the caller provides it.
538
- # Also make everything visible, because presumably the caller gave a field name because they want it to be seen.
539
- modifier = FieldModifier(visible=True, key_field=False, editable=editable)
540
-
541
393
  # Build the form using only those fields that are desired.
542
394
  builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
543
395
  for field_name in fields:
544
396
  field_def = field_defs.get(field_name)
545
397
  if field_def is None:
546
398
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
547
- builder.add_field(modifier.modify_field(field_def))
548
-
549
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list,
550
- record_image_data_list=image_data, group_by_field=group_by,
551
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
552
- try:
553
- self.user.timeout_seconds = self.timeout_seconds
554
- response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
555
- except ReadTimeout:
556
- raise SapioDialogTimeoutException()
557
- finally:
558
- self.user.timeout_seconds = self._original_timeout
559
- if response is None:
560
- raise SapioUserCancelledException()
561
- return response
562
-
563
- def multi_type_table_dialog(self,
564
- title: str,
565
- msg: str,
566
- fields: list[(str, str) | AbstractVeloxFieldDefinition],
567
- row_contents: list[list[SapioRecord | FieldMap]],
568
- *,
569
- default_modifier: FieldModifier | None = None,
570
- field_modifiers: dict[str, FieldModifier] | None = None,
571
- data_type: str = "Default",
572
- display_name: str | None = None,
573
- plural_display_name: str | None = None) -> list[FieldMap]:
574
- """
575
- Create a table dialog where the user may input data into the fields of the table. The table is constructed from
576
- a given list of records of multiple data types or field maps. Provided field names must match with field names
577
- from the data type definition of the given records. The fields that are displayed will have their default value
578
- be that of the fields on the given records or field maps.
579
-
580
- Makes webservice calls to get the data type field definitions of the given records if they weren't
581
- previously cached.
582
-
583
- :param title: The title of the dialog.
584
- :param msg: The message to display in the dialog.
585
- :param fields: A list of objects representing the fields in the table. This could either be a two-element tuple
586
- where the first element is a data type name and the second is a field name, or it could be a field
587
- definition. If it is the former, a query will be made to find the field definition matching tht data type.
588
- The data type names of the fields must match the data type names of the records in the row contents.
589
- See the description of row_contents for what to do if you want to construct a field that pulls from a field
590
- map.
591
- If two fields share the same field name, an exception will be thrown. This is even true in the case where
592
- the data type name of the fields is different. If you wish to display two fields from two data types with
593
- the same name, then you must provide a FieldModifier for at least one of the fields where prepend_data_type
594
- is True in order to make that field's name unique again. Note that if you do this for a field, the mapping
595
- of record to field name will use the unedited field name, but the return results of this function will
596
- use the edited field name in the results dictionary for a row.
597
- :param row_contents: A list where each element is another list representing the records or a field map that will
598
- be used to populate the columns of the table. If the data type of a given record doesn't match any of the
599
- data type names of the given fields, then it will not be used.
600
- This list can contain up to one field map, which are fields not tied to a record. This is so that you can
601
- create abstract field definition not tied to a specific record in the system. If you want to define an
602
- abstract field that pulls from the field map in the row contents, then you must set the data type name to
603
- Default.
604
- If a record of a given data type appears more than once in one of the inner-lists of the row contents, or
605
- there is more than one field map, then an exception will be thrown, as there is no way of distinguishing
606
- which record should be used for a field, and not all fields could have their values combined across multiple
607
- records.
608
- The row contents may have an inner-list which is missing a record of a data type that matches one of the
609
- fields. In this case, the field value for that row under that column will be null.
610
- The inner-list does not need to be sorted in any way, as this function will map the inner-list contents to
611
- fields as necessary.
612
- The inner-list may contain null elements; these will simply be discarded by this function.
613
- :param default_modifier: A default field modifier that will be applied to the given fields. This can be used to
614
- make field definitions from the system behave differently than their system values. If this value is None,
615
- then a default field modifier is created that causes all specified fields to be both visible and not key
616
- fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
617
- disabled by default in order to have the columns in the table respect the order of the fields as they are
618
- provided to this function.)
619
- :param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
620
- the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
621
- None, then the default modifier will be used.
622
- :param data_type: The data type name for the temporary data type that will be created for this table.
623
- :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
624
- name.
625
- :param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
626
- the display name + "s".
627
- :return: A list of dictionaries mapping the data field names of the given field definitions to the response
628
- value from the user for that field for each row.
629
- """
630
- # Set the default modifier to make all fields visible and not key if no default was provided.
631
- if default_modifier is None:
632
- default_modifier = FieldModifier(visible=True, key_field=False)
633
- # To make things simpler, treat null field modifiers as an empty dict.
634
- if field_modifiers is None:
635
- field_modifiers = {}
636
-
637
- # Construct the final fields list from the possible field objects.
638
- final_fields: list[AbstractVeloxFieldDefinition] = []
639
- # Keep track of whether any given field name appears more than once, as two fields could have the same
640
- # field name but different data types. In this case, the user should provide a field modifier or field
641
- # definition that changes one of the field names.
642
- field_names: list[str] = []
643
- for field in fields:
644
- # Find the field definition for this field object.
645
- if isinstance(field, tuple):
646
- field_def: AbstractVeloxFieldDefinition = self.dt_cache.get_fields_for_type(field[0]).get(field[1])
647
- elif isinstance(field, AbstractVeloxFieldDefinition):
648
- field_def: AbstractVeloxFieldDefinition = field
649
- else:
650
- raise SapioException("Unrecognized field object.")
651
-
652
- # Locate the modifier for this field and store the modified field.
653
- name: str = field_def.data_field_name
654
- modifier: FieldModifier = field_modifiers.get(name, default_modifier)
655
- field_def: AbstractVeloxFieldDefinition = modifier.modify_field(field_def)
656
- final_fields.append(field_def)
657
-
658
- # Verify that this field name isn't a duplicate.
659
- # The field name may have changed due to the modifier.
660
- name: str = field_def.data_field_name
661
- if name in field_names:
662
- raise SapioException(f"The field name \"{name}\" appears more than once in the given fields. "
663
- f"If you have provided two fields with the same name but different data types, "
664
- f"consider providing a FieldModifier where prepend_data_type is true for one of "
665
- f"the fields so that the field names will be different.")
666
- field_names.append(name)
667
-
668
- # Get the values for each row.
669
- values: list[dict[str, FieldValue]] = []
670
- for row in row_contents:
671
- # The final values for this row:
672
- row_values: dict[str, FieldValue] = {}
673
-
674
- # Map the records for this row by their data type. If a field map is provided, its data type is Default.
675
- row_records: dict[str, SapioRecord | FieldMap] = {}
676
- for rec in row:
677
- # Toss out null elements.
678
- if rec is None:
679
- continue
680
- # Map records to their data type name. Map field maps to Default.
681
- dt: str = "Default" if isinstance(rec, dict) else rec.data_type_name
682
- # Warn if the same data type name appears more than once.
683
- if dt in row_records:
684
- raise SapioException(f"The data type \"{dt}\" appears more than once in the given row contents.")
685
- row_records[dt] = rec
686
-
687
- # Get the field values from the above records.
688
- for field in final_fields:
689
- # Find the object that corresponds to this field given its data type name.
690
- record: SapioRecord | FieldMap | None = row_records.get(field.data_type_name)
691
- # This could be either a record, a field map, or null. Convert any records to field maps.
692
- if not isinstance(record, dict) and record is not None:
693
- record: FieldMap | None = AliasUtil.to_field_map_lists([record])[0]
694
-
695
- # Find out if this field had its data type prepended to it. If this is the case, then we need to find
696
- # the true data field name before retrieving the value from the field map.
697
- name: str = field.data_field_name
698
- if field_modifiers.get(name, default_modifier).prepend_data_type is True:
699
- name = name.split(".")[1]
700
-
701
- # Set the value for this particular field.
702
- row_values[field.data_field_name] = record.get(name) if record else None
703
- values.append(row_values)
704
-
705
- if display_name is None:
706
- display_name = data_type
707
- if plural_display_name is None:
708
- plural_display_name = display_name + "s"
709
-
710
- builder = FormBuilder(data_type, display_name, plural_display_name)
711
- for field in final_fields:
712
- builder.add_field(field)
713
-
714
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
715
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
716
- try:
717
- self.user.timeout_seconds = self.timeout_seconds
718
- response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
719
- except ReadTimeout:
720
- raise SapioDialogTimeoutException()
721
- finally:
722
- self.user.timeout_seconds = self._original_timeout
399
+ if editable is not None:
400
+ field_def.editable = editable
401
+ field_def.visible = True
402
+ # Key fields display their columns in order before all non-key fields.
403
+ # Unmark key fields so that the column order is respected exactly as the caller provides it.
404
+ field_def.key_field = False
405
+ builder.add_field(field_def)
406
+
407
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list)
408
+ response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
723
409
  if response is None:
724
410
  raise SapioUserCancelledException()
725
411
  return response
@@ -767,15 +453,8 @@ class CallbackUtil:
767
453
  raise SapioException(f"The data type \"{data_type}\" does not have a layout by the name "
768
454
  f"\"{layout_name}\" in the system.")
769
455
 
770
- request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list,
771
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
772
- try:
773
- self.user.timeout_seconds = self.timeout_seconds
774
- response: bool = self.callback.data_record_form_view_dialog(request)
775
- except ReadTimeout:
776
- raise SapioDialogTimeoutException()
777
- finally:
778
- self.user.timeout_seconds = self._original_timeout
456
+ request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list)
457
+ response: bool = self.callback.data_record_form_view_dialog(request)
779
458
  if not response:
780
459
  raise SapioUserCancelledException()
781
460
 
@@ -785,8 +464,7 @@ class CallbackUtil:
785
464
  values: list[FieldMap],
786
465
  multi_select: bool = True,
787
466
  *,
788
- data_type: str = "Default",
789
- display_name: str | None = None,
467
+ display_name: str = "Default",
790
468
  plural_display_name: str | None = None) -> list[FieldMap]:
791
469
  """
792
470
  Create a selection dialog for a list of field maps for the user to choose from. Requires that the caller
@@ -797,37 +475,24 @@ class CallbackUtil:
797
475
  they are provided in this list.
798
476
  :param values: The values to set for each row of the table.
799
477
  :param multi_select: Whether the user is able to select multiple rows from the list.
800
- :param data_type: The data type name for the temporary data type that will be created for this table.
801
- :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
802
- name.
478
+ :param display_name: The display name for the temporary data type that will be created.
803
479
  :param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
804
480
  the display name + "s".
805
481
  :return: A list of field maps corresponding to the chosen input field maps.
806
482
  """
807
- if display_name is None:
808
- display_name = data_type
809
483
  if plural_display_name is None:
810
484
  plural_display_name = display_name + "s"
811
485
 
812
- builder = FormBuilder(data_type, display_name, plural_display_name)
813
- for field in fields:
814
- builder.add_field(field)
815
-
816
- request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, values,
817
- multi_select=multi_select)
818
- try:
819
- self.user.timeout_seconds = self.timeout_seconds
820
- response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
821
- except ReadTimeout:
822
- raise SapioDialogTimeoutException()
823
- finally:
824
- self.user.timeout_seconds = self._original_timeout
486
+ # Build the form using only those fields that are desired.
487
+ request = DataRecordSelectionRequest(display_name, plural_display_name,
488
+ fields, values, msg, multi_select)
489
+ response: list[FieldMap] | None = self.callback.show_data_record_selection_dialog(request)
825
490
  if response is None:
826
491
  raise SapioUserCancelledException()
827
492
  return response
828
493
 
829
494
  def record_selection_dialog(self, msg: str, fields: list[str], records: list[SapioRecord],
830
- multi_select: bool = True) -> list[SapioRecord]:
495
+ multi_select: bool = True) -> list[FieldMap]:
831
496
  """
832
497
  Create a record selection dialog for a list of records for the user to choose from. Provided field names must
833
498
  match fields on the definition of the data type of the given records.
@@ -840,10 +505,8 @@ class CallbackUtil:
840
505
  they are provided in this list.
841
506
  :param records: The records to display as rows in the table.
842
507
  :param multi_select: Whether the user is able to select multiple records from the list.
843
- :return: A list of the selected records.
508
+ :return: A list of field maps corresponding to the chosen input records.
844
509
  """
845
- if not records:
846
- raise SapioException("No records provided.")
847
510
  data_types: set[str] = {x.data_type_name for x in records}
848
511
  if len(data_types) > 1:
849
512
  raise SapioException("Multiple data type names encountered in records list for record table popup.")
@@ -858,28 +521,21 @@ class CallbackUtil:
858
521
  type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
859
522
  field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
860
523
 
861
- # Key fields display their columns in order before all non-key fields.
862
- # Unmark key fields so that the column order is respected exactly as the caller provides it.
863
- # Also make everything visible, because presumably the caller give a field name because they want it to be seen.
864
- modifier = FieldModifier(visible=True, key_field=False)
865
-
866
524
  # Build the form using only those fields that are desired.
867
- builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
525
+ field_def_list: list = []
868
526
  for field_name in fields:
869
527
  field_def = field_defs.get(field_name)
870
528
  if field_def is None:
871
529
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
872
- builder.add_field(modifier.modify_field(field_def))
873
-
874
- request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, field_map_list,
875
- multi_select=multi_select)
876
- try:
877
- self.user.timeout_seconds = self.timeout_seconds
878
- response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
879
- except ReadTimeout:
880
- raise SapioDialogTimeoutException()
881
- finally:
882
- self.user.timeout_seconds = self._original_timeout
530
+ field_def.visible = True
531
+ # Key fields display their columns in order before all non-key fields.
532
+ # Unmark key fields so that the column order is respected exactly as the caller provides it.
533
+ field_def.key_field = False
534
+ field_def_list.append(field_def)
535
+
536
+ request = DataRecordSelectionRequest(type_def.display_name, type_def.plural_display_name,
537
+ field_def_list, field_map_list, msg, multi_select)
538
+ response: list[FieldMap] | None = self.callback.show_data_record_selection_dialog(request)
883
539
  if response is None:
884
540
  raise SapioUserCancelledException()
885
541
  # Map the field maps in the response back to the record they come from, returning the chosen record instead of
@@ -946,7 +602,7 @@ class CallbackUtil:
946
602
 
947
603
  # If CustomReportCriteria was provided, it must be wrapped as a CustomReport.
948
604
  if isinstance(custom_search, CustomReportCriteria):
949
- custom_search: CustomReport = CustomReport(False, [], custom_search)
605
+ custom_search: CustomReport = CustomReport(False, None, custom_search)
950
606
  # If a string was provided, locate the report criteria for the predefined search in the system matching this
951
607
  # name.
952
608
  if isinstance(custom_search, str):
@@ -955,13 +611,7 @@ class CallbackUtil:
955
611
  request = InputSelectionRequest(data_type, msg, search_types, only_key_fields, record_blacklist,
956
612
  record_whitelist, preselected_records, custom_search, scan_criteria,
957
613
  multi_select)
958
- try:
959
- self.user.timeout_seconds = self.timeout_seconds
960
- response: list[DataRecord] | None = self.callback.show_input_selection_dialog(request)
961
- except ReadTimeout:
962
- raise SapioDialogTimeoutException()
963
- finally:
964
- self.user.timeout_seconds = self._original_timeout
614
+ response: list[DataRecord] | None = self.callback.show_input_selection_dialog(request)
965
615
  if response is None:
966
616
  raise SapioUserCancelledException()
967
617
  return RecordHandler(self.user).wrap_models(response, wrapper_type)
@@ -985,21 +635,14 @@ class CallbackUtil:
985
635
  for field in additional_fields:
986
636
  builder.add_field(field)
987
637
  temp_dt = builder.get_temporary_data_type()
988
- request = ESigningRequestPojo(title, msg, show_comment, temp_dt,
989
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
990
- try:
991
- self.user.timeout_seconds = self.timeout_seconds
992
- response: ESigningResponsePojo | None = self.callback.show_esign_dialog(request)
993
- except ReadTimeout:
994
- raise SapioDialogTimeoutException()
995
- finally:
996
- self.user.timeout_seconds = self._original_timeout
638
+ request = ESigningRequestPojo(title, msg, show_comment, temp_dt)
639
+ response: ESigningResponsePojo | None = self.callback.show_esign_dialog(request)
997
640
  if response is None:
998
641
  raise SapioUserCancelledException()
999
642
  return response
1000
643
 
1001
644
  def request_file(self, title: str, exts: list[str] | None = None,
1002
- show_image_editor: bool = False, show_camera_button: bool = False) -> tuple[str, bytes]:
645
+ show_image_editor: bool = False, show_camera_button: bool = False) -> (str, bytes):
1003
646
  """
1004
647
  Request a single file from the user.
1005
648
 
@@ -1024,13 +667,7 @@ class CallbackUtil:
1024
667
  return sink.consume_data(chunk, io_obj)
1025
668
 
1026
669
  request = FilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
1027
- try:
1028
- self.user.timeout_seconds = self.timeout_seconds
1029
- file_path: str | None = self.callback.show_file_dialog(request, do_consume)
1030
- except ReadTimeout:
1031
- raise SapioDialogTimeoutException()
1032
- finally:
1033
- self.user.timeout_seconds = self._original_timeout
670
+ file_path: str | None = self.callback.show_file_dialog(request, do_consume)
1034
671
  if file_path is None:
1035
672
  raise SapioUserCancelledException()
1036
673
 
@@ -1038,7 +675,7 @@ class CallbackUtil:
1038
675
  return file_path, sink.data
1039
676
 
1040
677
  def request_files(self, title: str, exts: list[str] | None = None,
1041
- show_image_editor: bool = False, show_camera_button: bool = False) -> dict[str, bytes]:
678
+ show_image_editor: bool = False, show_camera_button: bool = False):
1042
679
  """
1043
680
  Request multiple files from the user.
1044
681
 
@@ -1055,13 +692,7 @@ class CallbackUtil:
1055
692
  exts: list[str] = []
1056
693
 
1057
694
  request = MultiFilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
1058
- try:
1059
- self.user.timeout_seconds = self.timeout_seconds
1060
- file_paths: list[str] | None = self.callback.show_multi_file_dialog(request)
1061
- except ReadTimeout:
1062
- raise SapioDialogTimeoutException()
1063
- finally:
1064
- self.user.timeout_seconds = self._original_timeout
695
+ file_paths: list[str] | None = self.callback.show_multi_file_dialog(request)
1065
696
  if not file_paths:
1066
697
  raise SapioUserCancelledException()
1067
698
 
@@ -1075,7 +706,7 @@ class CallbackUtil:
1075
706
  return ret_dict
1076
707
 
1077
708
  @staticmethod
1078
- def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]) -> None:
709
+ def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]):
1079
710
  """
1080
711
  Verify that the provided file was read (i.e. the file path and file bytes aren't None or empty) and that it
1081
712
  has the correct file extension. Raises a user error exception if something about the file is incorrect.
@@ -1089,7 +720,7 @@ class CallbackUtil:
1089
720
  if len(allowed_extensions) != 0:
1090
721
  matches: bool = False
1091
722
  for ext in allowed_extensions:
1092
- if file_path.endswith("." + ext.lstrip(".")):
723
+ if file_path.endswith("." + ext):
1093
724
  matches = True
1094
725
  break
1095
726
  if matches is False:
@@ -1103,85 +734,5 @@ class CallbackUtil:
1103
734
  :param file_name: The name of the file.
1104
735
  :param file_data: The data of the file, provided as either a string or as a bytes array.
1105
736
  """
1106
- data = io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data)
737
+ data = io.StringIO(file_data) if isinstance(file_data, str) else io.BytesIO(file_data)
1107
738
  self.callback.send_file(file_name, False, data)
1108
-
1109
- def write_zip_file(self, zip_name: str, files: dict[str, str | bytes]) -> None:
1110
- """
1111
- Send a collection of files to the user in a zip file.
1112
-
1113
- :param zip_name: The name of the zip file.
1114
- :param files: A dictionary of the files to add to the zip file.
1115
- """
1116
- data = io.BytesIO(FileUtil.zip_files(files))
1117
- self.callback.send_file(zip_name, False, data)
1118
-
1119
-
1120
- class FieldModifier:
1121
- """
1122
- A FieldModifier can be used to update the settings of a field definition from the system.
1123
- """
1124
- prepend_data_type: bool
1125
- display_name: str | None
1126
- required: bool | None
1127
- editable: bool | None
1128
- visible: bool | None
1129
- key_field: bool | None
1130
- column_width: int | None
1131
-
1132
- def __init__(self, *, prepend_data_type: bool = False,
1133
- display_name: str | None = None, required: bool | None = None, editable: bool | None = None,
1134
- visible: bool | None = None, key_field: bool | None = None, column_width: int | None = None):
1135
- """
1136
- If any values are given as None then that value will not be changed on the given field.
1137
-
1138
- :param prepend_data_type: If true, prepends the data type name of the field to the data field name. For example,
1139
- if a field has a data type name X and a data field name Y, then the field name would become "X.Y". This is
1140
- useful for cases where you have the same field name on two different data types and want to distinguish one
1141
- or both of them.
1142
- :param display_name: Change the display name.
1143
- :param required: Change the required status.
1144
- :param editable: Change the editable status.
1145
- :param visible: Change the visible status.
1146
- :param key_field: Change the key field status.
1147
- :param column_width: Change the column width.
1148
- """
1149
- self.prepend_data_type = prepend_data_type
1150
- self.display_name = display_name
1151
- self.required = required
1152
- self.editable = editable
1153
- self.visible = visible
1154
- self.key_field = key_field
1155
- self.column_width = column_width
1156
-
1157
- def modify_field(self, field: AbstractVeloxFieldDefinition) -> AbstractVeloxFieldDefinition:
1158
- """
1159
- Apply modifications to a given field.
1160
-
1161
- :param field: The field to modify.
1162
- :return: A copy of the input field with the modifications applied.
1163
- """
1164
- field = copy_field(field)
1165
- if self.prepend_data_type is True:
1166
- field._data_field_name = field.data_field_name + "." + field.data_field_name
1167
- if self.display_name is not None:
1168
- field.display_name = self.display_name
1169
- if self.required is not None:
1170
- field.required = self.required
1171
- if self.editable is not None:
1172
- field.editable = self.editable
1173
- if self.visible is not None:
1174
- field.visible = self.visible
1175
- if self.key_field is not None:
1176
- field.key_field = self.key_field
1177
- if self.column_width is not None:
1178
- field.default_table_column_width = self.column_width
1179
- return field
1180
-
1181
-
1182
- def copy_field(field: AbstractVeloxFieldDefinition) -> AbstractVeloxFieldDefinition:
1183
- """
1184
- Create a copy of a given field definition. This is used to modify field definitions from the server for existing
1185
- data types without also modifying the field definition in the cache.
1186
- """
1187
- return FieldDefinitionParser.to_field_definition(field.to_json())