sapiopycommons 2024.8.28a314__tar.gz → 2024.8.28a315__tar.gz

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 (61) hide show
  1. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/PKG-INFO +1 -1
  2. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/pyproject.toml +1 -1
  3. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/callbacks/callback_util.py +133 -37
  4. sapiopycommons-2024.8.28a315/src/sapiopycommons/customreport/column_builder.py +60 -0
  5. sapiopycommons-2024.8.28a315/src/sapiopycommons/customreport/custom_report_builder.py +125 -0
  6. sapiopycommons-2024.8.28a315/src/sapiopycommons/customreport/term_builder.py +299 -0
  7. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/datatype/attachment_util.py +11 -10
  8. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/eln/experiment_handler.py +209 -48
  9. sapiopycommons-2024.8.28a315/src/sapiopycommons/eln/experiment_report_util.py +118 -0
  10. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/complex_data_loader.py +5 -4
  11. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/file_bridge.py +15 -14
  12. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/file_bridge_handler.py +27 -5
  13. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/file_data_handler.py +2 -5
  14. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/file_util.py +38 -5
  15. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/file_validator.py +26 -11
  16. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/file_writer.py +44 -15
  17. sapiopycommons-2024.8.28a315/src/sapiopycommons/general/aliases.py +226 -0
  18. sapiopycommons-2024.8.28a315/src/sapiopycommons/general/audit_log.py +196 -0
  19. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/general/custom_report_util.py +34 -32
  20. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/general/popup_util.py +17 -0
  21. sapiopycommons-2024.8.28a315/src/sapiopycommons/general/sapio_links.py +50 -0
  22. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/general/time_util.py +40 -0
  23. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/multimodal/multimodal_data.py +0 -1
  24. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/processtracking/endpoints.py +22 -22
  25. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/recordmodel/record_handler.py +228 -77
  26. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/rules/eln_rule_handler.py +34 -25
  27. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/rules/on_save_rule_handler.py +34 -31
  28. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/webhook/webhook_handlers.py +90 -26
  29. sapiopycommons-2024.8.28a315/src/sapiopycommons/webhook/webservice_handlers.py +67 -0
  30. sapiopycommons-2024.8.28a315/tests/_do_not_add_init_py_here +0 -0
  31. sapiopycommons-2024.8.28a314/src/sapiopycommons/eln/experiment_report_util.py +0 -214
  32. sapiopycommons-2024.8.28a314/src/sapiopycommons/general/aliases.py +0 -82
  33. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/.gitignore +0 -0
  34. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/LICENSE +0 -0
  35. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/README.md +0 -0
  36. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/__init__.py +0 -0
  37. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/callbacks/__init__.py +0 -0
  38. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/chem/IndigoMolecules.py +0 -0
  39. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/chem/Molecules.py +0 -0
  40. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/chem/__init__.py +0 -0
  41. {sapiopycommons-2024.8.28a314/src/sapiopycommons/datatype → sapiopycommons-2024.8.28a315/src/sapiopycommons/customreport}/__init__.py +0 -0
  42. {sapiopycommons-2024.8.28a314/src/sapiopycommons/eln → sapiopycommons-2024.8.28a315/src/sapiopycommons/datatype}/__init__.py +0 -0
  43. {sapiopycommons-2024.8.28a314/src/sapiopycommons/files → sapiopycommons-2024.8.28a315/src/sapiopycommons/eln}/__init__.py +0 -0
  44. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/eln/plate_designer.py +0 -0
  45. {sapiopycommons-2024.8.28a314/src/sapiopycommons/general → sapiopycommons-2024.8.28a315/src/sapiopycommons/files}/__init__.py +0 -0
  46. {sapiopycommons-2024.8.28a314/src/sapiopycommons/processtracking → sapiopycommons-2024.8.28a315/src/sapiopycommons/general}/__init__.py +0 -0
  47. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/general/accession_service.py +0 -0
  48. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/general/exceptions.py +0 -0
  49. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/general/storage_util.py +0 -0
  50. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/multimodal/multimodal.py +0 -0
  51. {sapiopycommons-2024.8.28a314/src/sapiopycommons/recordmodel → sapiopycommons-2024.8.28a315/src/sapiopycommons/processtracking}/__init__.py +0 -0
  52. {sapiopycommons-2024.8.28a314/src/sapiopycommons/rules → sapiopycommons-2024.8.28a315/src/sapiopycommons/recordmodel}/__init__.py +0 -0
  53. {sapiopycommons-2024.8.28a314/src/sapiopycommons/webhook → sapiopycommons-2024.8.28a315/src/sapiopycommons/rules}/__init__.py +0 -0
  54. /sapiopycommons-2024.8.28a314/tests/_do_not_add_init_py_here → /sapiopycommons-2024.8.28a315/src/sapiopycommons/webhook/__init__.py +0 -0
  55. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/tests/accession_test.py +0 -0
  56. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/tests/bio_reg_test.py +0 -0
  57. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/tests/chem_test.py +0 -0
  58. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/tests/data_type_models.py +0 -0
  59. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/tests/kappa.chains.fasta +0 -0
  60. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/tests/mafft_test.py +0 -0
  61. {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/tests/test.gb +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2024.8.28a314
3
+ Version: 2024.8.28a315
4
4
  Summary: Official Sapio Python API Utilities Package
5
5
  Project-URL: Homepage, https://github.com/sapiosciences
6
6
  Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sapiopycommons"
7
- version='2024.08.28a314'
7
+ version='2024.08.28a315'
8
8
  authors = [
9
9
  { name="Jonathan Steck", email="jsteck@sapiosciences.com" },
10
10
  { name="Yechen Qiao", email="yqiao@sapiosciences.com" },
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import io
4
- from typing import Any
4
+ from weakref import WeakValueDictionary
5
5
 
6
6
  from sapiopylib.rest.ClientCallbackService import ClientCallback
7
7
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
@@ -15,16 +15,17 @@ from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefi
15
15
  from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import OptionDialogRequest, ListDialogRequest, \
16
16
  FormEntryDialogRequest, InputDialogCriteria, TableEntryDialogRequest, ESigningRequestPojo, \
17
17
  DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, MultiFilePromptRequest, \
18
- TempTableSelectionRequest
18
+ TempTableSelectionRequest, DisplayPopupRequest, PopupType
19
19
  from sapiopylib.rest.pojo.webhook.ClientCallbackResult import ESigningResponsePojo
20
- from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
21
20
  from sapiopylib.rest.pojo.webhook.WebhookEnums import FormAccessLevel, ScanToSelectCriteria, SearchType
22
21
  from sapiopylib.rest.utils.DataTypeCacheManager import DataTypeCacheManager
23
22
  from sapiopylib.rest.utils.FormBuilder import FormBuilder
24
23
  from sapiopylib.rest.utils.recorddatasinks import InMemoryRecordDataSink
25
24
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
26
25
 
27
- from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier
26
+ from sapiopycommons.files.file_util import FileUtil
27
+ from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier, FieldValue, \
28
+ UserIdentifier
28
29
  from sapiopycommons.general.custom_report_util import CustomReportUtil
29
30
  from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException
30
31
  from sapiopycommons.recordmodel.record_handler import RecordHandler
@@ -37,26 +38,86 @@ class CallbackUtil:
37
38
  width_pixels: int | None
38
39
  width_percent: float | None
39
40
 
40
- def __init__(self, context: SapioWebhookContext | SapioUser):
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):
41
57
  """
42
58
  :param context: The current webhook context or a user object to send requests from.
43
59
  """
44
- self.user = context if isinstance(context, SapioUser) else context.user
60
+ if self.__initialized:
61
+ return
62
+ self.__initialized = True
63
+
64
+ self.user = AliasUtil.to_sapio_user(context)
45
65
  self.callback = DataMgmtServer.get_client_callback(self.user)
46
66
  self.dt_cache = DataTypeCacheManager(self.user)
47
67
  self.width_pixels = None
48
68
  self.width_percent = None
49
69
 
50
- def set_dialog_width(self, width_pixels: int | None, width_percent: float | None):
70
+ def set_dialog_width(self, width_pixels: int | None = None, width_percent: float | None = None):
51
71
  """
52
72
  Set the width that dialogs will appear as for those dialogs that support specifying their width.
53
73
 
54
74
  :param width_pixels: The number of pixels wide that dialogs will appear as.
55
- :param width_percent: The percentage of the client's screen width 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.
56
77
  """
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.")
57
80
  self.width_pixels = width_pixels
58
81
  self.width_percent = width_percent
59
-
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
+
60
121
  def option_dialog(self, title: str, msg: str, options: list[str], default_option: int = 0,
61
122
  user_can_cancel: bool = False) -> str:
62
123
  """
@@ -71,7 +132,8 @@ class CallbackUtil:
71
132
  SapioUserCancelledException is thrown.
72
133
  :return: The name of the button that the user selected.
73
134
  """
74
- request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel)
135
+ request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
136
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
75
137
  response: int | None = self.callback.show_option_dialog(request)
76
138
  if response is None:
77
139
  raise SapioUserCancelledException()
@@ -111,16 +173,20 @@ class CallbackUtil:
111
173
  """
112
174
  return self.option_dialog(title, msg, ["Yes", "No"], 0 if default_yes else 1, False) == "Yes"
113
175
 
114
- def list_dialog(self, title: str, options: list[str], multi_select: bool = False) -> list[str]:
176
+ def list_dialog(self, title: str, options: list[str], multi_select: bool = False,
177
+ preselected_values: list[str] | None = None) -> list[str]:
115
178
  """
116
179
  Create a list dialog with the given options for the user to choose from.
117
180
 
118
181
  :param title: The title of the dialog.
119
182
  :param options: The list options that the user has to choose from.
120
183
  :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.
121
186
  :return: The list of options that the user selected.
122
187
  """
123
- request = ListDialogRequest(title, multi_select, options)
188
+ request = ListDialogRequest(title, multi_select, options, preselected_values,
189
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
124
190
  response: list[str] | None = self.callback.show_list_dialog(request)
125
191
  if response is None:
126
192
  raise SapioUserCancelledException()
@@ -163,8 +229,6 @@ class CallbackUtil:
163
229
  builder = FormBuilder(data_type, display_name, plural_display_name)
164
230
  for field_def in fields:
165
231
  field_name = field_def.data_field_name
166
- if values and hasattr(field_def, "default_value"):
167
- field_def.default_value = values.get(field_name)
168
232
  column: int = 0
169
233
  span: int = 4
170
234
  if column_positions and field_name in column_positions:
@@ -173,7 +237,8 @@ class CallbackUtil:
173
237
  span = position[1]
174
238
  builder.add_field(field_def, column, span)
175
239
 
176
- request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type())
240
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
241
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
177
242
  response: FieldMap | None = self.callback.show_form_entry_dialog(request)
178
243
  if response is None:
179
244
  raise SapioUserCancelledException()
@@ -215,13 +280,13 @@ class CallbackUtil:
215
280
  modifier = FieldModifier(visible=True, editable=editable)
216
281
 
217
282
  # Build the form using only those fields that are desired.
283
+ values: dict[str, FieldValue] = {}
218
284
  builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
219
285
  for field_name in fields:
220
286
  field_def = field_defs.get(field_name)
221
287
  if field_def is None:
222
288
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
223
- if hasattr(field_def, "default_value"):
224
- field_def.default_value = record.get_field_value(field_name)
289
+ values[field_name] = record.get_field_value(field_name)
225
290
  column: int = 0
226
291
  span: int = 4
227
292
  if column_positions and field_name in column_positions:
@@ -230,13 +295,14 @@ class CallbackUtil:
230
295
  span = position[1]
231
296
  builder.add_field(modifier.modify_field(field_def), column, span)
232
297
 
233
- request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type())
298
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
299
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
234
300
  response: FieldMap | None = self.callback.show_form_entry_dialog(request)
235
301
  if response is None:
236
302
  raise SapioUserCancelledException()
237
303
  return response
238
304
 
239
- def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) -> Any:
305
+ def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) -> FieldValue:
240
306
  """
241
307
  Create an input dialog where the user must input data for a singular field.
242
308
 
@@ -245,8 +311,9 @@ class CallbackUtil:
245
311
  :param field: The definition for a field that the user must provide input to.
246
312
  :return: The response value from the user for the given field.
247
313
  """
248
- request = InputDialogCriteria(title, msg, field, self.width_pixels, self.width_percent)
249
- response: Any | None = self.callback.show_input_dialog(request)
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)
250
317
  if response is None:
251
318
  raise SapioUserCancelledException()
252
319
  return response
@@ -322,6 +389,8 @@ class CallbackUtil:
322
389
  msg: str,
323
390
  fields: list[AbstractVeloxFieldDefinition],
324
391
  values: list[FieldMap],
392
+ group_by: str | None = None,
393
+ image_data: list[bytes] | None = None,
325
394
  *,
326
395
  data_type: str = "Default",
327
396
  display_name: str | None = None,
@@ -335,6 +404,10 @@ class CallbackUtil:
335
404
  :param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
336
405
  they are provided in this list.
337
406
  :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.
338
411
  :param data_type: The data type name for the temporary data type that will be created for this table.
339
412
  :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
340
413
  name.
@@ -356,7 +429,9 @@ class CallbackUtil:
356
429
  for field in fields:
357
430
  builder.add_field(modifier.modify_field(field))
358
431
 
359
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values)
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)
360
435
  response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
361
436
  if response is None:
362
437
  raise SapioUserCancelledException()
@@ -367,7 +442,9 @@ class CallbackUtil:
367
442
  msg: str,
368
443
  fields: list[str],
369
444
  records: list[SapioRecord],
370
- editable: bool | None = True) -> list[FieldMap]:
445
+ editable: bool | None = True,
446
+ group_by: str | None = None,
447
+ image_data: list[bytes] | None = None) -> list[FieldMap]:
371
448
  """
372
449
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
373
450
  a given list of records of a singular type. Provided field names must match fields on the definition of the data
@@ -384,6 +461,10 @@ class CallbackUtil:
384
461
  they are provided in this list.
385
462
  :param editable: If true, all fields are displayed as editable. If false, all fields are displayed as
386
463
  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.
387
468
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
388
469
  value from the user for that field for each row.
389
470
  """
@@ -410,7 +491,9 @@ class CallbackUtil:
410
491
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
411
492
  builder.add_field(modifier.modify_field(field_def))
412
493
 
413
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list)
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)
414
497
  response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
415
498
  if response is None:
416
499
  raise SapioUserCancelledException()
@@ -522,10 +605,10 @@ class CallbackUtil:
522
605
  field_names.append(name)
523
606
 
524
607
  # Get the values for each row.
525
- values: list[dict[str, Any]] = []
608
+ values: list[dict[str, FieldValue]] = []
526
609
  for row in row_contents:
527
610
  # The final values for this row:
528
- row_values: dict[str, Any] = {}
611
+ row_values: dict[str, FieldValue] = {}
529
612
 
530
613
  # Map the records for this row by their data type. If a field map is provided, its data type is Default.
531
614
  row_records: dict[str, SapioRecord | FieldMap] = {}
@@ -567,7 +650,8 @@ class CallbackUtil:
567
650
  for field in final_fields:
568
651
  builder.add_field(field)
569
652
 
570
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values)
653
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
654
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
571
655
  response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
572
656
  if response is None:
573
657
  raise SapioUserCancelledException()
@@ -616,7 +700,8 @@ class CallbackUtil:
616
700
  raise SapioException(f"The data type \"{data_type}\" does not have a layout by the name "
617
701
  f"\"{layout_name}\" in the system.")
618
702
 
619
- request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list)
703
+ request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list,
704
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
620
705
  response: bool = self.callback.data_record_form_view_dialog(request)
621
706
  if not response:
622
707
  raise SapioUserCancelledException()
@@ -663,7 +748,7 @@ class CallbackUtil:
663
748
  return response
664
749
 
665
750
  def record_selection_dialog(self, msg: str, fields: list[str], records: list[SapioRecord],
666
- multi_select: bool = True) -> list[FieldMap]:
751
+ multi_select: bool = True) -> list[SapioRecord]:
667
752
  """
668
753
  Create a record selection dialog for a list of records for the user to choose from. Provided field names must
669
754
  match fields on the definition of the data type of the given records.
@@ -676,7 +761,7 @@ class CallbackUtil:
676
761
  they are provided in this list.
677
762
  :param records: The records to display as rows in the table.
678
763
  :param multi_select: Whether the user is able to select multiple records from the list.
679
- :return: A list of field maps corresponding to the chosen input records.
764
+ :return: A list of the selected records.
680
765
  """
681
766
  data_types: set[str] = {x.data_type_name for x in records}
682
767
  if len(data_types) > 1:
@@ -774,7 +859,7 @@ class CallbackUtil:
774
859
 
775
860
  # If CustomReportCriteria was provided, it must be wrapped as a CustomReport.
776
861
  if isinstance(custom_search, CustomReportCriteria):
777
- custom_search: CustomReport = CustomReport(False, None, custom_search)
862
+ custom_search: CustomReport = CustomReport(False, [], custom_search)
778
863
  # If a string was provided, locate the report criteria for the predefined search in the system matching this
779
864
  # name.
780
865
  if isinstance(custom_search, str):
@@ -807,14 +892,15 @@ class CallbackUtil:
807
892
  for field in additional_fields:
808
893
  builder.add_field(field)
809
894
  temp_dt = builder.get_temporary_data_type()
810
- request = ESigningRequestPojo(title, msg, show_comment, temp_dt)
895
+ request = ESigningRequestPojo(title, msg, show_comment, temp_dt,
896
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
811
897
  response: ESigningResponsePojo | None = self.callback.show_esign_dialog(request)
812
898
  if response is None:
813
899
  raise SapioUserCancelledException()
814
900
  return response
815
901
 
816
902
  def request_file(self, title: str, exts: list[str] | None = None,
817
- show_image_editor: bool = False, show_camera_button: bool = False) -> (str, bytes):
903
+ show_image_editor: bool = False, show_camera_button: bool = False) -> tuple[str, bytes]:
818
904
  """
819
905
  Request a single file from the user.
820
906
 
@@ -847,7 +933,7 @@ class CallbackUtil:
847
933
  return file_path, sink.data
848
934
 
849
935
  def request_files(self, title: str, exts: list[str] | None = None,
850
- show_image_editor: bool = False, show_camera_button: bool = False):
936
+ show_image_editor: bool = False, show_camera_button: bool = False) -> dict[str, bytes]:
851
937
  """
852
938
  Request multiple files from the user.
853
939
 
@@ -878,7 +964,7 @@ class CallbackUtil:
878
964
  return ret_dict
879
965
 
880
966
  @staticmethod
881
- def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]):
967
+ def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]) -> None:
882
968
  """
883
969
  Verify that the provided file was read (i.e. the file path and file bytes aren't None or empty) and that it
884
970
  has the correct file extension. Raises a user error exception if something about the file is incorrect.
@@ -892,7 +978,7 @@ class CallbackUtil:
892
978
  if len(allowed_extensions) != 0:
893
979
  matches: bool = False
894
980
  for ext in allowed_extensions:
895
- if file_path.endswith("." + ext):
981
+ if file_path.endswith("." + ext.lstrip(".")):
896
982
  matches = True
897
983
  break
898
984
  if matches is False:
@@ -906,9 +992,19 @@ class CallbackUtil:
906
992
  :param file_name: The name of the file.
907
993
  :param file_data: The data of the file, provided as either a string or as a bytes array.
908
994
  """
909
- data = io.StringIO(file_data) if isinstance(file_data, str) else io.BytesIO(file_data)
995
+ data = io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data)
910
996
  self.callback.send_file(file_name, False, data)
911
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
+
912
1008
 
913
1009
  class FieldModifier:
914
1010
  """
@@ -0,0 +1,60 @@
1
+ from sapiopylib.rest.pojo.CustomReport import ReportColumn
2
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
3
+
4
+ from sapiopycommons.general.aliases import DataTypeIdentifier, FieldIdentifier, AliasUtil
5
+ from sapiopycommons.general.exceptions import SapioException
6
+
7
+ # The system fields that every record has and their field types. System fields aren't generated as record model fields
8
+ # for all platform version, hence the need to create a dict for them in the off chance that they're not present on
9
+ # the model wrapper.
10
+ SYSTEM_FIELDS: dict[str, FieldType] = {
11
+ "DataRecordName": FieldType.IDENTIFIER,
12
+ "RecordId": FieldType.LONG,
13
+ "DateCreated": FieldType.DATE,
14
+ "CreatedBy": FieldType.STRING,
15
+ "VeloxLastModifiedDate": FieldType.DATE,
16
+ "VeloxLastModifiedBy": FieldType.STRING
17
+ }
18
+
19
+
20
+ class ColumnBuilder:
21
+ """
22
+ A class for building report columns for custom reports.
23
+ """
24
+ @staticmethod
25
+ def build_column(data_type: DataTypeIdentifier, field: FieldIdentifier, field_type: FieldType | None = None) \
26
+ -> ReportColumn:
27
+ """
28
+ Build a ReportColumn from a variety of possible inputs.
29
+
30
+ :param data_type: An object that can be used to identify a data type.
31
+ :param field: An object that can be used to identify a data field.
32
+ :param field_type: The field type of the provided field. This is only required if the field type cannot be
33
+ determined from the given data type and field, which occurs when the given field is a string and the
34
+ given data type is not a wrapped record model or record model wrapper.
35
+ :return: A ReportColumn for the inputs.
36
+ """
37
+ # Get the data type and field names from the inputs.
38
+ data_type_name = AliasUtil.to_data_type_name(data_type)
39
+ field_name = AliasUtil.to_data_field_name(field)
40
+ if field_type is None:
41
+ field_type = ColumnBuilder.__field_type(data_type, field)
42
+ if field_type is None:
43
+ raise SapioException("The field_type parameter is required for the provided data_type and field inputs.")
44
+ return ReportColumn(data_type_name, field_name, field_type)
45
+
46
+ @staticmethod
47
+ def __field_type(data_type: DataTypeIdentifier, field: FieldIdentifier) -> FieldType | None:
48
+ """
49
+ Given a record model wrapper and a field name, return the field type for that field. Accounts for system fields.
50
+
51
+ :param data_type: The record model wrapper that the field is on.
52
+ :param field: The field name to return the type of.
53
+ :return: The field type of the given field name.
54
+ """
55
+ # Check if the field name is a system field. If it us, use the field type defined in this file.
56
+ field_name: str = AliasUtil.to_data_field_name(field)
57
+ if field_name in SYSTEM_FIELDS:
58
+ return SYSTEM_FIELDS.get(field_name)
59
+ # Otherwise, check if the field type can be found from the wrapper.
60
+ return AliasUtil.to_field_type(field_name, data_type)
@@ -0,0 +1,125 @@
1
+ from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReportCriteria, AbstractReportTerm, \
2
+ ExplicitJoinDefinition, RelatedRecordCriteria, QueryRestriction, FieldCompareReportTerm
3
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
4
+
5
+ from sapiopycommons.customreport.column_builder import ColumnBuilder
6
+ from sapiopycommons.general.aliases import DataTypeIdentifier, FieldIdentifier, AliasUtil, SapioRecord
7
+ from sapiopycommons.general.exceptions import SapioException
8
+
9
+
10
+ class CustomReportBuilder:
11
+ """
12
+ A class used for building custom reports. Look into using the TermBuilder and ColumnBuilder classes for building
13
+ parts of a custom report.
14
+ """
15
+ root_data_type: DataTypeIdentifier
16
+ data_type_name: str
17
+ root_term: AbstractReportTerm | None
18
+ record_criteria: RelatedRecordCriteria
19
+ column_list: list[ReportColumn]
20
+ join_list: list[ExplicitJoinDefinition]
21
+
22
+ def __init__(self, root_data_type: DataTypeIdentifier):
23
+ """
24
+ :param root_data_type: An object that can be used to identify a data type name. Used as the root data type name
25
+ of this search.
26
+ """
27
+ self.root_data_type = root_data_type
28
+ self.data_type_name = AliasUtil.to_data_type_name(root_data_type)
29
+ self.root_term = None
30
+ self.record_criteria = RelatedRecordCriteria(QueryRestriction.QUERY_ALL)
31
+ self.column_list = []
32
+ self.join_list = []
33
+
34
+ def has_root_term(self) -> bool:
35
+ """
36
+ :return: Whether this report builder has had its root term set.
37
+ """
38
+ return self.root_term is not None
39
+
40
+ def set_root_term(self, term: AbstractReportTerm) -> None:
41
+ """
42
+ Set the root term of the report. Use the TermBuilder class to construct the report terms.
43
+
44
+ :param term: The term to set as the root term.
45
+ """
46
+ self.root_term = term
47
+
48
+ def has_columns(self) -> bool:
49
+ """
50
+ :return: Whether this report builder has any report columns.
51
+ """
52
+ return bool(self.column_list)
53
+
54
+ def add_column(self, field: FieldIdentifier, field_type: FieldType = None,
55
+ *, data_type: DataTypeIdentifier | None = None) -> None:
56
+ """
57
+ Add a column to this report builder.
58
+
59
+ :param field: An object that can be used to identify a data field.
60
+ :param field_type: The field type of the provided field. This is only required if the field type cannot be
61
+ determined from the given data type and field, which occurs when the given field is a string and the
62
+ given data type is not a wrapped record model or record model wrapper.
63
+ :param data_type: An object that can be used to identify a data type. If not provided, uses the root data type
64
+ provided when this builder was initialized. You'll only want to specify this value when adding a column
65
+ that is from a different data type than the root data type.
66
+ """
67
+ if data_type is None:
68
+ data_type = self.root_data_type
69
+ self.column_list.append(ColumnBuilder.build_column(data_type, field, field_type))
70
+
71
+ def add_columns(self, fields: list[FieldIdentifier], *, data_type: DataTypeIdentifier | None = None) -> None:
72
+ """
73
+ Add columns to this report builder.
74
+
75
+ :param fields: A list of objects that can be used to identify data fields.
76
+ :param data_type: An object that can be used to identify a data type. If not provided, uses the root data type
77
+ provided when this builder was initialized. You'll only want to specify this value when adding a column
78
+ that is from a different data type than the root data type.
79
+ """
80
+ for field in fields:
81
+ self.add_column(field, data_type=data_type)
82
+
83
+ def set_query_restriction(self, base_record: SapioRecord, search_related: QueryRestriction) -> None:
84
+ """
85
+ Set a restriction on the report for this report builder such that the returned results must be related in
86
+ some way to the provided base record. Without this, the report searches all records in the system that match the
87
+ root term.
88
+
89
+ :param base_record: The base record to run the search from.
90
+ :param search_related: Determine the relationship of the related records that can appear in the search, be those
91
+ children, parents, descendants, or ancestors.
92
+ """
93
+ if search_related == QueryRestriction.QUERY_ALL:
94
+ raise SapioException("The search_related must be something other than QUERY_ALL when setting a query restriction.")
95
+ self.record_criteria = RelatedRecordCriteria(search_related,
96
+ AliasUtil.to_record_id(base_record),
97
+ AliasUtil.to_data_type_name(base_record))
98
+
99
+ def add_join(self, comparison_term: FieldCompareReportTerm) -> None:
100
+ """
101
+ Add a join statement to this report builder.
102
+
103
+ :param comparison_term: The field comparison term to join with. The left side data type name of this term will
104
+ be the data type that is joined against.
105
+ """
106
+ self.join_list.append(ExplicitJoinDefinition(comparison_term.left_data_type_name, comparison_term))
107
+
108
+ def build_report_criteria(self, page_size: int = 0, page_number: int = -1, case_sensitive: bool = False,
109
+ owner_restriction_set: list[str] = None) -> CustomReportCriteria:
110
+ """
111
+ Generate a CustomReportCriteria using the column list, root term, and root data type from this report builder.
112
+ You can use the CustomReportManager or CustomReportUtil to run the constructed report.
113
+
114
+ :param page_size: The page size of the custom report.
115
+ :param page_number: The page number of the current report.
116
+ :param case_sensitive: When searching texts, should the search be case-sensitive?
117
+ :param owner_restriction_set: Specifies to only return records if the record is owned by this list of usernames.
118
+ :return: A CustomReportCriteria from this report builder.
119
+ """
120
+ if not self.has_root_term():
121
+ raise SapioException("Cannot build a report with no root term.")
122
+ if not self.has_columns():
123
+ raise SapioException("Cannot build a report with no columns.")
124
+ return CustomReportCriteria(self.column_list, self.root_term, self.record_criteria, self.data_type_name,
125
+ case_sensitive, page_size, page_number, owner_restriction_set, self.join_list)