sapiopycommons 2024.8.27a310__py3-none-any.whl → 2024.8.28a313__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 (37) hide show
  1. sapiopycommons/callbacks/callback_util.py +69 -407
  2. sapiopycommons/chem/IndigoMolecules.py +0 -1
  3. sapiopycommons/chem/Molecules.py +0 -1
  4. sapiopycommons/datatype/attachment_util.py +10 -11
  5. sapiopycommons/eln/experiment_handler.py +48 -209
  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 +10 -50
  10. sapiopycommons/files/file_validator.py +6 -92
  11. sapiopycommons/files/file_writer.py +15 -44
  12. sapiopycommons/general/aliases.py +3 -147
  13. sapiopycommons/general/custom_report_util.py +37 -211
  14. sapiopycommons/general/popup_util.py +0 -17
  15. sapiopycommons/general/time_util.py +0 -40
  16. sapiopycommons/processtracking/endpoints.py +22 -22
  17. sapiopycommons/recordmodel/record_handler.py +97 -481
  18. sapiopycommons/rules/eln_rule_handler.py +25 -34
  19. sapiopycommons/rules/on_save_rule_handler.py +31 -34
  20. sapiopycommons/webhook/webhook_handlers.py +26 -147
  21. {sapiopycommons-2024.8.27a310.dist-info → sapiopycommons-2024.8.28a313.dist-info}/METADATA +2 -4
  22. sapiopycommons-2024.8.28a313.dist-info/RECORD +38 -0
  23. sapiopycommons/customreport/__init__.py +0 -0
  24. sapiopycommons/customreport/column_builder.py +0 -60
  25. sapiopycommons/customreport/custom_report_builder.py +0 -125
  26. sapiopycommons/customreport/term_builder.py +0 -299
  27. sapiopycommons/eln/experiment_report_util.py +0 -118
  28. sapiopycommons/files/file_bridge_handler.py +0 -340
  29. sapiopycommons/general/accession_service.py +0 -375
  30. sapiopycommons/general/audit_log.py +0 -196
  31. sapiopycommons/general/sapio_links.py +0 -50
  32. sapiopycommons/multimodal/multimodal.py +0 -146
  33. sapiopycommons/multimodal/multimodal_data.py +0 -486
  34. sapiopycommons/webhook/webservice_handlers.py +0 -67
  35. sapiopycommons-2024.8.27a310.dist-info/RECORD +0 -50
  36. {sapiopycommons-2024.8.27a310.dist-info → sapiopycommons-2024.8.28a313.dist-info}/WHEEL +0 -0
  37. {sapiopycommons-2024.8.27a310.dist-info → sapiopycommons-2024.8.28a313.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,5 @@
1
- from __future__ import annotations
2
-
3
1
  import io
4
- from weakref import WeakValueDictionary
2
+ from typing import Any
5
3
 
6
4
  from sapiopylib.rest.ClientCallbackService import ClientCallback
7
5
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
@@ -11,21 +9,20 @@ from sapiopylib.rest.pojo.DataRecord import DataRecord
11
9
  from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
12
10
  from sapiopylib.rest.pojo.datatype.DataTypeLayout import DataTypeLayout
13
11
  from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition, VeloxStringFieldDefinition, \
14
- VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition, FieldDefinitionParser
12
+ VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition
15
13
  from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import OptionDialogRequest, ListDialogRequest, \
16
14
  FormEntryDialogRequest, InputDialogCriteria, TableEntryDialogRequest, ESigningRequestPojo, \
17
- DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, MultiFilePromptRequest, \
18
- TempTableSelectionRequest, DisplayPopupRequest, PopupType
15
+ DataRecordSelectionRequest, DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, \
16
+ MultiFilePromptRequest
19
17
  from sapiopylib.rest.pojo.webhook.ClientCallbackResult import ESigningResponsePojo
18
+ from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
20
19
  from sapiopylib.rest.pojo.webhook.WebhookEnums import FormAccessLevel, ScanToSelectCriteria, SearchType
21
20
  from sapiopylib.rest.utils.DataTypeCacheManager import DataTypeCacheManager
22
21
  from sapiopylib.rest.utils.FormBuilder import FormBuilder
23
22
  from sapiopylib.rest.utils.recorddatasinks import InMemoryRecordDataSink
24
23
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
25
24
 
26
- from sapiopycommons.files.file_util import FileUtil
27
- from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier, FieldValue, \
28
- UserIdentifier
25
+ from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier
29
26
  from sapiopycommons.general.custom_report_util import CustomReportUtil
30
27
  from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException
31
28
  from sapiopycommons.recordmodel.record_handler import RecordHandler
@@ -38,86 +35,26 @@ class CallbackUtil:
38
35
  width_pixels: int | None
39
36
  width_percent: float | None
40
37
 
41
- __instances: WeakValueDictionary[SapioUser, CallbackUtil] = WeakValueDictionary()
42
- __initialized: bool
43
-
44
- def __new__(cls, context: UserIdentifier):
45
- """
46
- :param context: The current webhook context or a user object to send requests from.
47
- """
48
- user = AliasUtil.to_sapio_user(context)
49
- obj = cls.__instances.get(user)
50
- if not obj:
51
- obj = object.__new__(cls)
52
- obj.__initialized = False
53
- cls.__instances[user] = obj
54
- return obj
55
-
56
- def __init__(self, context: UserIdentifier):
38
+ def __init__(self, context: SapioWebhookContext | SapioUser):
57
39
  """
58
40
  :param context: The current webhook context or a user object to send requests from.
59
41
  """
60
- if self.__initialized:
61
- return
62
- self.__initialized = True
63
-
64
- self.user = AliasUtil.to_sapio_user(context)
42
+ self.user = context if isinstance(context, SapioUser) else context.user
65
43
  self.callback = DataMgmtServer.get_client_callback(self.user)
66
44
  self.dt_cache = DataTypeCacheManager(self.user)
67
45
  self.width_pixels = None
68
46
  self.width_percent = None
69
47
 
70
- 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):
71
49
  """
72
50
  Set the width that dialogs will appear as for those dialogs that support specifying their width.
73
51
 
74
52
  :param width_pixels: The number of pixels wide that dialogs will appear as.
75
- :param width_percent: The percentage (as a value between 0 and 1) of the client's screen width that dialogs
76
- will appear as.
53
+ :param width_percent: The percentage of the client's screen width that dialogs will appear as.
77
54
  """
78
- if width_pixels is not None and width_percent is not None:
79
- raise SapioException("Cannot set both width_pixels and width_percent at once.")
80
55
  self.width_pixels = width_pixels
81
56
  self.width_percent = width_percent
82
-
83
- def toaster_popup(self, message: str, title: str = "", popup_type: PopupType = PopupType.Info) -> None:
84
- """
85
- Display a toaster popup in the bottom right corner of the user's screen.
86
-
87
- :param message: The message to display in the toaster.
88
- :param title: The title to display at the top of the toaster.
89
- :param popup_type: The popup type to use for the toaster. This controls the color that the toaster appears with.
90
- Info is blue, Success is green, Warning is yellow, and Error is red
91
- """
92
- self.callback.display_popup(DisplayPopupRequest(title, message, popup_type))
93
-
94
- def display_info(self, message: str) -> None:
95
- """
96
- Display an info message to the user in a dialog. Repeated calls to this function will append the new messages
97
- to the same dialog if it is still opened by the user.
98
-
99
- :param message: The message to display to the user.
100
- """
101
- self.callback.display_info(message)
102
-
103
- def display_warning(self, message: str) -> None:
104
- """
105
- Display a warning message to the user in a dialog. Repeated calls to this function will append the new messages
106
- to the same dialog if it is still opened by the user.
107
-
108
- :param message: The message to display to the user.
109
- """
110
- self.callback.display_warning(message)
111
-
112
- def display_error(self, message: str) -> None:
113
- """
114
- Display an error message to the user in a dialog. Repeated calls to this function will append the new messages
115
- to the same dialog if it is still opened by the user.
116
-
117
- :param message: The message to display to the user.
118
- """
119
- self.callback.display_error(message)
120
-
57
+
121
58
  def option_dialog(self, title: str, msg: str, options: list[str], default_option: int = 0,
122
59
  user_can_cancel: bool = False) -> str:
123
60
  """
@@ -132,8 +69,7 @@ class CallbackUtil:
132
69
  SapioUserCancelledException is thrown.
133
70
  :return: The name of the button that the user selected.
134
71
  """
135
- request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
136
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
72
+ request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel)
137
73
  response: int | None = self.callback.show_option_dialog(request)
138
74
  if response is None:
139
75
  raise SapioUserCancelledException()
@@ -173,20 +109,16 @@ class CallbackUtil:
173
109
  """
174
110
  return self.option_dialog(title, msg, ["Yes", "No"], 0 if default_yes else 1, False) == "Yes"
175
111
 
176
- def list_dialog(self, title: str, options: list[str], multi_select: bool = False,
177
- preselected_values: list[str] | None = None) -> list[str]:
112
+ def list_dialog(self, title: str, options: list[str], multi_select: bool = False) -> list[str]:
178
113
  """
179
114
  Create a list dialog with the given options for the user to choose from.
180
115
 
181
116
  :param title: The title of the dialog.
182
117
  :param options: The list options that the user has to choose from.
183
118
  :param multi_select: Whether the user is able to select multiple options from the list.
184
- :param preselected_values: A list of values that will already be selected when the list dialog is created. The
185
- user can unselect these values if they want to.
186
119
  :return: The list of options that the user selected.
187
120
  """
188
- request = ListDialogRequest(title, multi_select, options, preselected_values,
189
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
121
+ request = ListDialogRequest(title, multi_select, options)
190
122
  response: list[str] | None = self.callback.show_list_dialog(request)
191
123
  if response is None:
192
124
  raise SapioUserCancelledException()
@@ -229,6 +161,8 @@ class CallbackUtil:
229
161
  builder = FormBuilder(data_type, display_name, plural_display_name)
230
162
  for field_def in fields:
231
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)
232
166
  column: int = 0
233
167
  span: int = 4
234
168
  if column_positions and field_name in column_positions:
@@ -237,8 +171,7 @@ class CallbackUtil:
237
171
  span = position[1]
238
172
  builder.add_field(field_def, column, span)
239
173
 
240
- request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
241
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
174
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type())
242
175
  response: FieldMap | None = self.callback.show_form_entry_dialog(request)
243
176
  if response is None:
244
177
  raise SapioUserCancelledException()
@@ -276,33 +209,32 @@ class CallbackUtil:
276
209
  type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
277
210
  field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
278
211
 
279
- # Make everything visible, because presumably the caller gave a field name because they want it to be seen.
280
- modifier = FieldModifier(visible=True, editable=editable)
281
-
282
212
  # Build the form using only those fields that are desired.
283
- values: dict[str, FieldValue] = {}
284
213
  builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
285
214
  for field_name in fields:
286
215
  field_def = field_defs.get(field_name)
287
216
  if field_def is None:
288
217
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
289
- 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)
290
223
  column: int = 0
291
224
  span: int = 4
292
225
  if column_positions and field_name in column_positions:
293
226
  position = column_positions.get(field_name)
294
227
  column = position[0]
295
228
  span = position[1]
296
- builder.add_field(modifier.modify_field(field_def), column, span)
229
+ builder.add_field(field_def, column, span)
297
230
 
298
- request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
299
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
231
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type())
300
232
  response: FieldMap | None = self.callback.show_form_entry_dialog(request)
301
233
  if response is None:
302
234
  raise SapioUserCancelledException()
303
235
  return response
304
236
 
305
- def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) -> FieldValue:
237
+ def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) -> Any:
306
238
  """
307
239
  Create an input dialog where the user must input data for a singular field.
308
240
 
@@ -311,9 +243,8 @@ class CallbackUtil:
311
243
  :param field: The definition for a field that the user must provide input to.
312
244
  :return: The response value from the user for the given field.
313
245
  """
314
- request = InputDialogCriteria(title, msg, field,
315
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
316
- response: FieldValue | None = self.callback.show_input_dialog(request)
246
+ request = InputDialogCriteria(title, msg, field, self.width_pixels, self.width_percent)
247
+ response: Any | None = self.callback.show_input_dialog(request)
317
248
  if response is None:
318
249
  raise SapioUserCancelledException()
319
250
  return response
@@ -328,7 +259,7 @@ class CallbackUtil:
328
259
  :param field_name: The name and display name of the string field.
329
260
  :param default_value: The default value to place into the string field, if any.
330
261
  :param max_length: The max length of the string value. If not provided, uses the length of the default value.
331
- 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.
332
263
  :param editable: Whether the field is editable by the user.
333
264
  :param kwargs: Any additional keyword arguments to pass to the field definition.
334
265
  :return: The string that the user input into the dialog.
@@ -389,8 +320,6 @@ class CallbackUtil:
389
320
  msg: str,
390
321
  fields: list[AbstractVeloxFieldDefinition],
391
322
  values: list[FieldMap],
392
- group_by: str | None = None,
393
- image_data: list[bytes] | None = None,
394
323
  *,
395
324
  data_type: str = "Default",
396
325
  display_name: str | None = None,
@@ -404,10 +333,6 @@ class CallbackUtil:
404
333
  :param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
405
334
  they are provided in this list.
406
335
  :param values: The values to set for each row of the table.
407
- :param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
408
- The user may remove this grouping if they want to.
409
- :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
410
- the image data list corresponds to the element at the same index in the values list.
411
336
  :param data_type: The data type name for the temporary data type that will be created for this table.
412
337
  :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
413
338
  name.
@@ -421,17 +346,11 @@ class CallbackUtil:
421
346
  if plural_display_name is None:
422
347
  plural_display_name = display_name + "s"
423
348
 
424
- # Key fields display their columns in order before all non-key fields.
425
- # Unmark key fields so that the column order is respected exactly as the caller provides it.
426
- modifier = FieldModifier(key_field=False)
427
-
428
349
  builder = FormBuilder(data_type, display_name, plural_display_name)
429
- for field in fields:
430
- builder.add_field(modifier.modify_field(field))
350
+ for column in fields:
351
+ builder.add_field(column)
431
352
 
432
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
433
- record_image_data_list=image_data, group_by_field=group_by,
434
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
353
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values)
435
354
  response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
436
355
  if response is None:
437
356
  raise SapioUserCancelledException()
@@ -442,14 +361,11 @@ class CallbackUtil:
442
361
  msg: str,
443
362
  fields: list[str],
444
363
  records: list[SapioRecord],
445
- editable: bool | None = True,
446
- group_by: str | None = None,
447
- image_data: list[bytes] | None = None) -> list[FieldMap]:
364
+ editable: bool | None = True) -> list[FieldMap]:
448
365
  """
449
366
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
450
- a given list of records of a singular type. Provided field names must match fields on the definition of the data
451
- type of the given records. The fields that are displayed will have their default value be that of the fields on
452
- 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.
453
369
 
454
370
  Makes webservice calls to get the data type definition and fields of the given records if they weren't
455
371
  previously cached.
@@ -461,10 +377,6 @@ class CallbackUtil:
461
377
  they are provided in this list.
462
378
  :param editable: If true, all fields are displayed as editable. If false, all fields are displayed as
463
379
  uneditable. If none, only those fields that are defined as editable by the data designer will be editable.
464
- :param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
465
- The user may remove this grouping if they want to.
466
- :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
467
- the image data list corresponds to the element at the same index in the records list.
468
380
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
469
381
  value from the user for that field for each row.
470
382
  """
@@ -478,180 +390,21 @@ class CallbackUtil:
478
390
  type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
479
391
  field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
480
392
 
481
- # Key fields display their columns in order before all non-key fields.
482
- # Unmark key fields so that the column order is respected exactly as the caller provides it.
483
- # Also make everything visible, because presumably the caller gave a field name because they want it to be seen.
484
- modifier = FieldModifier(visible=True, key_field=False, editable=editable)
485
-
486
393
  # Build the form using only those fields that are desired.
487
394
  builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
488
395
  for field_name in fields:
489
396
  field_def = field_defs.get(field_name)
490
397
  if field_def is None:
491
398
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
492
- builder.add_field(modifier.modify_field(field_def))
493
-
494
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list,
495
- record_image_data_list=image_data, group_by_field=group_by,
496
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
497
- response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
498
- if response is None:
499
- raise SapioUserCancelledException()
500
- return response
501
-
502
- def multi_type_table_dialog(self,
503
- title: str,
504
- msg: str,
505
- fields: list[(str, str) | AbstractVeloxFieldDefinition],
506
- row_contents: list[list[SapioRecord | FieldMap]],
507
- *,
508
- default_modifier: FieldModifier | None = None,
509
- field_modifiers: dict[str, FieldModifier] | None = None,
510
- data_type: str = "Default",
511
- display_name: str | None = None,
512
- plural_display_name: str | None = None) -> list[FieldMap]:
513
- """
514
- Create a table dialog where the user may input data into the fields of the table. The table is constructed from
515
- a given list of records of multiple data types or field maps. Provided field names must match with field names
516
- from the data type definition of the given records. The fields that are displayed will have their default value
517
- be that of the fields on the given records or field maps.
518
-
519
- Makes webservice calls to get the data type field definitions of the given records if they weren't
520
- previously cached.
521
-
522
- :param title: The title of the dialog.
523
- :param msg: The message to display in the dialog.
524
- :param fields: A list of objects representing the fields in the table. This could either be a two-element tuple
525
- where the first element is a data type name and the second is a field name, or it could be a field
526
- definition. If it is the former, a query will be made to find the field definition matching tht data type.
527
- The data type names of the fields must match the data type names of the records in the row contents.
528
- See the description of row_contents for what to do if you want to construct a field that pulls from a field
529
- map.
530
- If two fields share the same field name, an exception will be thrown. This is even true in the case where
531
- the data type name of the fields is different. If you wish to display two fields from two data types with
532
- the same name, then you must provide a FieldModifier for at least one of the fields where prepend_data_type
533
- is True in order to make that field's name unique again. Note that if you do this for a field, the mapping
534
- of record to field name will use the unedited field name, but the return results of this function will
535
- use the edited field name in the results dictionary for a row.
536
- :param row_contents: A list where each element is another list representing the records or a field map that will
537
- be used to populate the columns of the table. If the data type of a given record doesn't match any of the
538
- data type names of the given fields, then it will not be used.
539
- This list can contain up to one field map, which are fields not tied to a record. This is so that you can
540
- create abstract field definition not tied to a specific record in the system. If you want to define an
541
- abstract field that pulls from the field map in the row contents, then you must set the data type name to
542
- Default.
543
- If a record of a given data type appears more than once in one of the inner-lists of the row contents, or
544
- there is more than one field map, then an exception will be thrown, as there is no way of distinguishing
545
- which record should be used for a field, and not all fields could have their values combined across multiple
546
- records.
547
- The row contents may have an inner-list which is missing a record of a data type that matches one of the
548
- fields. In this case, the field value for that row under that column will be null.
549
- The inner-list does not need to be sorted in any way, as this function will map the inner-list contents to
550
- fields as necessary.
551
- The inner-list may contain null elements; these will simply be discarded by this function.
552
- :param default_modifier: A default field modifier that will be applied to the given fields. This can be used to
553
- make field definitions from the system behave differently than their system values. If this value is None,
554
- then a default field modifier is created that causes all specified fields to be both visible and not key
555
- fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
556
- disabled by default in order to have the columns in the table respect the order of the fields as they are
557
- provided to this function.)
558
- :param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
559
- the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
560
- None, then the default modifier will be used.
561
- :param data_type: The data type name for the temporary data type that will be created for this table.
562
- :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
563
- name.
564
- :param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
565
- the display name + "s".
566
- :return: A list of dictionaries mapping the data field names of the given field definitions to the response
567
- value from the user for that field for each row.
568
- """
569
- # Set the default modifier to make all fields visible and not key if no default was provided.
570
- if default_modifier is None:
571
- default_modifier = FieldModifier(visible=True, key_field=False)
572
- # To make things simpler, treat null field modifiers as an empty dict.
573
- if field_modifiers is None:
574
- field_modifiers = {}
575
-
576
- # Construct the final fields list from the possible field objects.
577
- final_fields: list[AbstractVeloxFieldDefinition] = []
578
- # Keep track of whether any given field name appears more than once, as two fields could have the same
579
- # field name but different data types. In this case, the user should provide a field modifier or field
580
- # definition that changes one of the field names.
581
- field_names: list[str] = []
582
- for field in fields:
583
- # Find the field definition for this field object.
584
- if isinstance(field, tuple):
585
- field_def: AbstractVeloxFieldDefinition = self.dt_cache.get_fields_for_type(field[0]).get(field[1])
586
- elif isinstance(field, AbstractVeloxFieldDefinition):
587
- field_def: AbstractVeloxFieldDefinition = field
588
- else:
589
- raise SapioException("Unrecognized field object.")
590
-
591
- # Locate the modifier for this field and store the modified field.
592
- name: str = field_def.data_field_name
593
- modifier: FieldModifier = field_modifiers.get(name, default_modifier)
594
- field_def: AbstractVeloxFieldDefinition = modifier.modify_field(field_def)
595
- final_fields.append(field_def)
596
-
597
- # Verify that this field name isn't a duplicate.
598
- # The field name may have changed due to the modifier.
599
- name: str = field_def.data_field_name
600
- if name in field_names:
601
- raise SapioException(f"The field name \"{name}\" appears more than once in the given fields. "
602
- f"If you have provided two fields with the same name but different data types, "
603
- f"consider providing a FieldModifier where prepend_data_type is true for one of "
604
- f"the fields so that the field names will be different.")
605
- field_names.append(name)
606
-
607
- # Get the values for each row.
608
- values: list[dict[str, FieldValue]] = []
609
- for row in row_contents:
610
- # The final values for this row:
611
- row_values: dict[str, FieldValue] = {}
612
-
613
- # Map the records for this row by their data type. If a field map is provided, its data type is Default.
614
- row_records: dict[str, SapioRecord | FieldMap] = {}
615
- for rec in row:
616
- # Toss out null elements.
617
- if rec is None:
618
- continue
619
- # Map records to their data type name. Map field maps to Default.
620
- dt: str = "Default" if isinstance(rec, dict) else rec.data_type_name
621
- # Warn if the same data type name appears more than once.
622
- if dt in row_records:
623
- raise SapioException(f"The data type \"{dt}\" appears more than once in the given row contents.")
624
- row_records[dt] = rec
625
-
626
- # Get the field values from the above records.
627
- for field in final_fields:
628
- # Find the object that corresponds to this field given its data type name.
629
- record: SapioRecord | FieldMap | None = row_records.get(field.data_type_name)
630
- # This could be either a record, a field map, or null. Convert any records to field maps.
631
- if not isinstance(record, dict) and record is not None:
632
- record: FieldMap | None = AliasUtil.to_field_map_lists([record])[0]
633
-
634
- # Find out if this field had its data type prepended to it. If this is the case, then we need to find
635
- # the true data field name before retrieving the value from the field map.
636
- name: str = field.data_field_name
637
- if field_modifiers.get(name, default_modifier).prepend_data_type is True:
638
- name = name.split(".")[1]
639
-
640
- # Set the value for this particular field.
641
- row_values[field.data_field_name] = record.get(name) if record else None
642
- values.append(row_values)
643
-
644
- if display_name is None:
645
- display_name = data_type
646
- if plural_display_name is None:
647
- plural_display_name = display_name + "s"
648
-
649
- builder = FormBuilder(data_type, display_name, plural_display_name)
650
- for field in final_fields:
651
- builder.add_field(field)
652
-
653
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
654
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
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)
655
408
  response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
656
409
  if response is None:
657
410
  raise SapioUserCancelledException()
@@ -700,8 +453,7 @@ class CallbackUtil:
700
453
  raise SapioException(f"The data type \"{data_type}\" does not have a layout by the name "
701
454
  f"\"{layout_name}\" in the system.")
702
455
 
703
- request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list,
704
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
456
+ request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list)
705
457
  response: bool = self.callback.data_record_form_view_dialog(request)
706
458
  if not response:
707
459
  raise SapioUserCancelledException()
@@ -712,8 +464,7 @@ class CallbackUtil:
712
464
  values: list[FieldMap],
713
465
  multi_select: bool = True,
714
466
  *,
715
- data_type: str = "Default",
716
- display_name: str | None = None,
467
+ display_name: str = "Default",
717
468
  plural_display_name: str | None = None) -> list[FieldMap]:
718
469
  """
719
470
  Create a selection dialog for a list of field maps for the user to choose from. Requires that the caller
@@ -724,31 +475,24 @@ class CallbackUtil:
724
475
  they are provided in this list.
725
476
  :param values: The values to set for each row of the table.
726
477
  :param multi_select: Whether the user is able to select multiple rows from the list.
727
- :param data_type: The data type name for the temporary data type that will be created for this table.
728
- :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
729
- name.
478
+ :param display_name: The display name for the temporary data type that will be created.
730
479
  :param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
731
480
  the display name + "s".
732
481
  :return: A list of field maps corresponding to the chosen input field maps.
733
482
  """
734
- if display_name is None:
735
- display_name = data_type
736
483
  if plural_display_name is None:
737
484
  plural_display_name = display_name + "s"
738
485
 
739
- builder = FormBuilder(data_type, display_name, plural_display_name)
740
- for field in fields:
741
- builder.add_field(field)
742
-
743
- request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, values,
744
- multi_select=multi_select)
745
- response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
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)
746
490
  if response is None:
747
491
  raise SapioUserCancelledException()
748
492
  return response
749
493
 
750
494
  def record_selection_dialog(self, msg: str, fields: list[str], records: list[SapioRecord],
751
- multi_select: bool = True) -> list[SapioRecord]:
495
+ multi_select: bool = True) -> list[FieldMap]:
752
496
  """
753
497
  Create a record selection dialog for a list of records for the user to choose from. Provided field names must
754
498
  match fields on the definition of the data type of the given records.
@@ -761,7 +505,7 @@ class CallbackUtil:
761
505
  they are provided in this list.
762
506
  :param records: The records to display as rows in the table.
763
507
  :param multi_select: Whether the user is able to select multiple records from the list.
764
- :return: A list of the selected records.
508
+ :return: A list of field maps corresponding to the chosen input records.
765
509
  """
766
510
  data_types: set[str] = {x.data_type_name for x in records}
767
511
  if len(data_types) > 1:
@@ -777,22 +521,21 @@ class CallbackUtil:
777
521
  type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
778
522
  field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
779
523
 
780
- # Key fields display their columns in order before all non-key fields.
781
- # Unmark key fields so that the column order is respected exactly as the caller provides it.
782
- # Also make everything visible, because presumably the caller give a field name because they want it to be seen.
783
- modifier = FieldModifier(visible=True, key_field=False)
784
-
785
524
  # Build the form using only those fields that are desired.
786
- builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
525
+ field_def_list: list = []
787
526
  for field_name in fields:
788
527
  field_def = field_defs.get(field_name)
789
528
  if field_def is None:
790
529
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
791
- builder.add_field(modifier.modify_field(field_def))
792
-
793
- request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, field_map_list,
794
- multi_select=multi_select)
795
- response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
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)
796
539
  if response is None:
797
540
  raise SapioUserCancelledException()
798
541
  # Map the field maps in the response back to the record they come from, returning the chosen record instead of
@@ -859,7 +602,7 @@ class CallbackUtil:
859
602
 
860
603
  # If CustomReportCriteria was provided, it must be wrapped as a CustomReport.
861
604
  if isinstance(custom_search, CustomReportCriteria):
862
- custom_search: CustomReport = CustomReport(False, [], custom_search)
605
+ custom_search: CustomReport = CustomReport(False, None, custom_search)
863
606
  # If a string was provided, locate the report criteria for the predefined search in the system matching this
864
607
  # name.
865
608
  if isinstance(custom_search, str):
@@ -892,15 +635,14 @@ class CallbackUtil:
892
635
  for field in additional_fields:
893
636
  builder.add_field(field)
894
637
  temp_dt = builder.get_temporary_data_type()
895
- request = ESigningRequestPojo(title, msg, show_comment, temp_dt,
896
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
638
+ request = ESigningRequestPojo(title, msg, show_comment, temp_dt)
897
639
  response: ESigningResponsePojo | None = self.callback.show_esign_dialog(request)
898
640
  if response is None:
899
641
  raise SapioUserCancelledException()
900
642
  return response
901
643
 
902
644
  def request_file(self, title: str, exts: list[str] | None = None,
903
- 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):
904
646
  """
905
647
  Request a single file from the user.
906
648
 
@@ -933,7 +675,7 @@ class CallbackUtil:
933
675
  return file_path, sink.data
934
676
 
935
677
  def request_files(self, title: str, exts: list[str] | None = None,
936
- show_image_editor: bool = False, show_camera_button: bool = False) -> dict[str, bytes]:
678
+ show_image_editor: bool = False, show_camera_button: bool = False):
937
679
  """
938
680
  Request multiple files from the user.
939
681
 
@@ -964,7 +706,7 @@ class CallbackUtil:
964
706
  return ret_dict
965
707
 
966
708
  @staticmethod
967
- 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]):
968
710
  """
969
711
  Verify that the provided file was read (i.e. the file path and file bytes aren't None or empty) and that it
970
712
  has the correct file extension. Raises a user error exception if something about the file is incorrect.
@@ -978,7 +720,7 @@ class CallbackUtil:
978
720
  if len(allowed_extensions) != 0:
979
721
  matches: bool = False
980
722
  for ext in allowed_extensions:
981
- if file_path.endswith("." + ext.lstrip(".")):
723
+ if file_path.endswith("." + ext):
982
724
  matches = True
983
725
  break
984
726
  if matches is False:
@@ -992,85 +734,5 @@ class CallbackUtil:
992
734
  :param file_name: The name of the file.
993
735
  :param file_data: The data of the file, provided as either a string or as a bytes array.
994
736
  """
995
- 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)
996
738
  self.callback.send_file(file_name, False, data)
997
-
998
- def write_zip_file(self, zip_name: str, files: dict[str, str | bytes]) -> None:
999
- """
1000
- Send a collection of files to the user in a zip file.
1001
-
1002
- :param zip_name: The name of the zip file.
1003
- :param files: A dictionary of the files to add to the zip file.
1004
- """
1005
- data = io.BytesIO(FileUtil.zip_files(files))
1006
- self.callback.send_file(zip_name, False, data)
1007
-
1008
-
1009
- class FieldModifier:
1010
- """
1011
- A FieldModifier can be used to update the settings of a field definition from the system.
1012
- """
1013
- prepend_data_type: bool
1014
- display_name: str | None
1015
- required: bool | None
1016
- editable: bool | None
1017
- visible: bool | None
1018
- key_field: bool | None
1019
- column_width: int | None
1020
-
1021
- def __init__(self, *, prepend_data_type: bool = False,
1022
- display_name: str | None = None, required: bool | None = None, editable: bool | None = None,
1023
- visible: bool | None = None, key_field: bool | None = None, column_width: int | None = None):
1024
- """
1025
- If any values are given as None then that value will not be changed on the given field.
1026
-
1027
- :param prepend_data_type: If true, prepends the data type name of the field to the data field name. For example,
1028
- 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
1029
- useful for cases where you have the same field name on two different data types and want to distinguish one
1030
- or both of them.
1031
- :param display_name: Change the display name.
1032
- :param required: Change the required status.
1033
- :param editable: Change the editable status.
1034
- :param visible: Change the visible status.
1035
- :param key_field: Change the key field status.
1036
- :param column_width: Change the column width.
1037
- """
1038
- self.prepend_data_type = prepend_data_type
1039
- self.display_name = display_name
1040
- self.required = required
1041
- self.editable = editable
1042
- self.visible = visible
1043
- self.key_field = key_field
1044
- self.column_width = column_width
1045
-
1046
- def modify_field(self, field: AbstractVeloxFieldDefinition) -> AbstractVeloxFieldDefinition:
1047
- """
1048
- Apply modifications to a given field.
1049
-
1050
- :param field: The field to modify.
1051
- :return: A copy of the input field with the modifications applied.
1052
- """
1053
- field = copy_field(field)
1054
- if self.prepend_data_type is True:
1055
- field._data_field_name = field.data_field_name + "." + field.data_field_name
1056
- if self.display_name is not None:
1057
- field.display_name = self.display_name
1058
- if self.required is not None:
1059
- field.required = self.required
1060
- if self.editable is not None:
1061
- field.editable = self.editable
1062
- if self.visible is not None:
1063
- field.visible = self.visible
1064
- if self.key_field is not None:
1065
- field.key_field = self.key_field
1066
- if self.column_width is not None:
1067
- field.default_table_column_width = self.column_width
1068
- return field
1069
-
1070
-
1071
- def copy_field(field: AbstractVeloxFieldDefinition) -> AbstractVeloxFieldDefinition:
1072
- """
1073
- Create a copy of a given field definition. This is used to modify field definitions from the server for existing
1074
- data types without also modifying the field definition in the cache.
1075
- """
1076
- return FieldDefinitionParser.to_field_definition(field.to_json())