sapiopycommons 2024.8.15a304__tar.gz → 2024.8.19a305__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 (60) hide show
  1. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/PKG-INFO +1 -1
  2. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/pyproject.toml +1 -1
  3. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/callbacks/callback_util.py +122 -25
  4. sapiopycommons-2024.8.19a305/src/sapiopycommons/customreport/column_builder.py +60 -0
  5. sapiopycommons-2024.8.19a305/src/sapiopycommons/customreport/custom_report_builder.py +125 -0
  6. sapiopycommons-2024.8.19a305/src/sapiopycommons/customreport/term_builder.py +296 -0
  7. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/datatype/attachment_util.py +15 -6
  8. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/eln/experiment_handler.py +193 -39
  9. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/complex_data_loader.py +1 -1
  10. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_bridge.py +1 -1
  11. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_bridge_handler.py +21 -0
  12. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_util.py +38 -5
  13. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_validator.py +21 -6
  14. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_writer.py +44 -15
  15. sapiopycommons-2024.8.19a305/src/sapiopycommons/general/aliases.py +173 -0
  16. sapiopycommons-2024.8.19a305/src/sapiopycommons/general/audit_log.py +200 -0
  17. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/popup_util.py +17 -0
  18. sapiopycommons-2024.8.19a305/src/sapiopycommons/general/sapio_links.py +48 -0
  19. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/time_util.py +40 -0
  20. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/recordmodel/record_handler.py +114 -17
  21. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/rules/eln_rule_handler.py +29 -22
  22. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/rules/on_save_rule_handler.py +29 -28
  23. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/webhook/webhook_handlers.py +90 -26
  24. sapiopycommons-2024.8.19a305/src/sapiopycommons/webhook/webservice_handlers.py +67 -0
  25. sapiopycommons-2024.8.19a305/tests/_do_not_add_init_py_here +0 -0
  26. sapiopycommons-2024.8.15a304/src/sapiopycommons/general/aliases.py +0 -82
  27. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/.gitignore +0 -0
  28. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/LICENSE +0 -0
  29. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/README.md +0 -0
  30. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/__init__.py +0 -0
  31. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/callbacks/__init__.py +0 -0
  32. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/chem/IndigoMolecules.py +0 -0
  33. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/chem/Molecules.py +0 -0
  34. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/chem/__init__.py +0 -0
  35. {sapiopycommons-2024.8.15a304/src/sapiopycommons/datatype → sapiopycommons-2024.8.19a305/src/sapiopycommons/customreport}/__init__.py +0 -0
  36. {sapiopycommons-2024.8.15a304/src/sapiopycommons/eln → sapiopycommons-2024.8.19a305/src/sapiopycommons/datatype}/__init__.py +0 -0
  37. {sapiopycommons-2024.8.15a304/src/sapiopycommons/files → sapiopycommons-2024.8.19a305/src/sapiopycommons/eln}/__init__.py +0 -0
  38. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/eln/experiment_report_util.py +0 -0
  39. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/eln/plate_designer.py +0 -0
  40. {sapiopycommons-2024.8.15a304/src/sapiopycommons/general → sapiopycommons-2024.8.19a305/src/sapiopycommons/files}/__init__.py +0 -0
  41. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_data_handler.py +0 -0
  42. {sapiopycommons-2024.8.15a304/src/sapiopycommons/processtracking → sapiopycommons-2024.8.19a305/src/sapiopycommons/general}/__init__.py +0 -0
  43. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/accession_service.py +0 -0
  44. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/custom_report_util.py +0 -0
  45. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/exceptions.py +0 -0
  46. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/storage_util.py +0 -0
  47. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/multimodal/multimodal.py +0 -0
  48. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/multimodal/multimodal_data.py +0 -0
  49. {sapiopycommons-2024.8.15a304/src/sapiopycommons/recordmodel → sapiopycommons-2024.8.19a305/src/sapiopycommons/processtracking}/__init__.py +0 -0
  50. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/processtracking/endpoints.py +0 -0
  51. {sapiopycommons-2024.8.15a304/src/sapiopycommons/rules → sapiopycommons-2024.8.19a305/src/sapiopycommons/recordmodel}/__init__.py +0 -0
  52. {sapiopycommons-2024.8.15a304/src/sapiopycommons/webhook → sapiopycommons-2024.8.19a305/src/sapiopycommons/rules}/__init__.py +0 -0
  53. /sapiopycommons-2024.8.15a304/tests/_do_not_add_init_py_here → /sapiopycommons-2024.8.19a305/src/sapiopycommons/webhook/__init__.py +0 -0
  54. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/accession_test.py +0 -0
  55. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/bio_reg_test.py +0 -0
  56. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/chem_test.py +0 -0
  57. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/data_type_models.py +0 -0
  58. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/kappa.chains.fasta +0 -0
  59. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/mafft_test.py +0 -0
  60. {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/test.gb +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2024.8.15a304
3
+ Version: 2024.8.19a305
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.15a304'
7
+ version='2024.08.19a305'
8
8
  authors = [
9
9
  { name="Jonathan Steck", email="jsteck@sapiosciences.com" },
10
10
  { name="Yechen Qiao", email="yqiao@sapiosciences.com" },
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import io
4
4
  from typing import Any
5
+ from weakref import WeakValueDictionary
5
6
 
6
7
  from sapiopylib.rest.ClientCallbackService import ClientCallback
7
8
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
@@ -15,7 +16,7 @@ from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefi
15
16
  from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import OptionDialogRequest, ListDialogRequest, \
16
17
  FormEntryDialogRequest, InputDialogCriteria, TableEntryDialogRequest, ESigningRequestPojo, \
17
18
  DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, MultiFilePromptRequest, \
18
- TempTableSelectionRequest
19
+ TempTableSelectionRequest, DisplayPopupRequest, PopupType
19
20
  from sapiopylib.rest.pojo.webhook.ClientCallbackResult import ESigningResponsePojo
20
21
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
21
22
  from sapiopylib.rest.pojo.webhook.WebhookEnums import FormAccessLevel, ScanToSelectCriteria, SearchType
@@ -24,6 +25,7 @@ from sapiopylib.rest.utils.FormBuilder import FormBuilder
24
25
  from sapiopylib.rest.utils.recorddatasinks import InMemoryRecordDataSink
25
26
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
26
27
 
28
+ from sapiopycommons.files.file_util import FileUtil
27
29
  from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier
28
30
  from sapiopycommons.general.custom_report_util import CustomReportUtil
29
31
  from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException
@@ -37,26 +39,86 @@ class CallbackUtil:
37
39
  width_pixels: int | None
38
40
  width_percent: float | None
39
41
 
42
+ __instances: WeakValueDictionary[SapioUser, CallbackUtil] = WeakValueDictionary()
43
+ __initialized: bool
44
+
45
+ def __new__(cls, context: SapioWebhookContext | SapioUser):
46
+ """
47
+ :param context: The current webhook context or a user object to send requests from.
48
+ """
49
+ user = context if isinstance(context, SapioUser) else context.user
50
+ obj = cls.__instances.get(user)
51
+ if not obj:
52
+ obj = object.__new__(cls)
53
+ obj.__initialized = False
54
+ cls.__instances[user] = obj
55
+ return obj
56
+
40
57
  def __init__(self, context: SapioWebhookContext | SapioUser):
41
58
  """
42
59
  :param context: The current webhook context or a user object to send requests from.
43
60
  """
61
+ if self.__initialized:
62
+ return
63
+ self.__initialized = True
64
+
44
65
  self.user = context if isinstance(context, SapioUser) else context.user
45
66
  self.callback = DataMgmtServer.get_client_callback(self.user)
46
67
  self.dt_cache = DataTypeCacheManager(self.user)
47
68
  self.width_pixels = None
48
69
  self.width_percent = None
49
70
 
50
- def set_dialog_width(self, width_pixels: int | None, width_percent: float | None):
71
+ def set_dialog_width(self, width_pixels: int | None = None, width_percent: float | None = None):
51
72
  """
52
73
  Set the width that dialogs will appear as for those dialogs that support specifying their width.
53
74
 
54
75
  :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.
76
+ :param width_percent: The percentage (as a value between 0 and 1) of the client's screen width that dialogs
77
+ will appear as.
56
78
  """
79
+ if width_pixels is not None and width_percent is not None:
80
+ raise SapioException("Cannot set both width_pixels and width_percent at once.")
57
81
  self.width_pixels = width_pixels
58
82
  self.width_percent = width_percent
59
-
83
+
84
+ def toaster_popup(self, message: str, title: str = "", popup_type: PopupType = PopupType.Info) -> None:
85
+ """
86
+ Display a toaster popup in the bottom right corner of the user's screen.
87
+
88
+ :param message: The message to display in the toaster.
89
+ :param title: The title to display at the top of the toaster.
90
+ :param popup_type: The popup type to use for the toaster. This controls the color that the toaster appears with.
91
+ Info is blue, Success is green, Warning is yellow, and Error is red
92
+ """
93
+ self.callback.display_popup(DisplayPopupRequest(title, message, popup_type))
94
+
95
+ def display_info(self, message: str) -> None:
96
+ """
97
+ Display an info message to the user in a dialog. Repeated calls to this function will append the new messages
98
+ to the same dialog if it is still opened by the user.
99
+
100
+ :param message: The message to display to the user.
101
+ """
102
+ self.callback.display_info(message)
103
+
104
+ def display_warning(self, message: str) -> None:
105
+ """
106
+ Display a warning message to the user in a dialog. Repeated calls to this function will append the new messages
107
+ to the same dialog if it is still opened by the user.
108
+
109
+ :param message: The message to display to the user.
110
+ """
111
+ self.callback.display_warning(message)
112
+
113
+ def display_error(self, message: str) -> None:
114
+ """
115
+ Display an error message to the user in a dialog. Repeated calls to this function will append the new messages
116
+ to the same dialog if it is still opened by the user.
117
+
118
+ :param message: The message to display to the user.
119
+ """
120
+ self.callback.display_error(message)
121
+
60
122
  def option_dialog(self, title: str, msg: str, options: list[str], default_option: int = 0,
61
123
  user_can_cancel: bool = False) -> str:
62
124
  """
@@ -71,7 +133,8 @@ class CallbackUtil:
71
133
  SapioUserCancelledException is thrown.
72
134
  :return: The name of the button that the user selected.
73
135
  """
74
- request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel)
136
+ request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
137
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
75
138
  response: int | None = self.callback.show_option_dialog(request)
76
139
  if response is None:
77
140
  raise SapioUserCancelledException()
@@ -111,16 +174,20 @@ class CallbackUtil:
111
174
  """
112
175
  return self.option_dialog(title, msg, ["Yes", "No"], 0 if default_yes else 1, False) == "Yes"
113
176
 
114
- def list_dialog(self, title: str, options: list[str], multi_select: bool = False) -> list[str]:
177
+ def list_dialog(self, title: str, options: list[str], multi_select: bool = False,
178
+ preselected_values: list[str] | None = None) -> list[str]:
115
179
  """
116
180
  Create a list dialog with the given options for the user to choose from.
117
181
 
118
182
  :param title: The title of the dialog.
119
183
  :param options: The list options that the user has to choose from.
120
184
  :param multi_select: Whether the user is able to select multiple options from the list.
185
+ :param preselected_values: A list of values that will already be selected when the list dialog is created. The
186
+ user can unselect these values if they want to.
121
187
  :return: The list of options that the user selected.
122
188
  """
123
- request = ListDialogRequest(title, multi_select, options)
189
+ request = ListDialogRequest(title, multi_select, options, preselected_values,
190
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
124
191
  response: list[str] | None = self.callback.show_list_dialog(request)
125
192
  if response is None:
126
193
  raise SapioUserCancelledException()
@@ -163,8 +230,6 @@ class CallbackUtil:
163
230
  builder = FormBuilder(data_type, display_name, plural_display_name)
164
231
  for field_def in fields:
165
232
  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
233
  column: int = 0
169
234
  span: int = 4
170
235
  if column_positions and field_name in column_positions:
@@ -173,7 +238,8 @@ class CallbackUtil:
173
238
  span = position[1]
174
239
  builder.add_field(field_def, column, span)
175
240
 
176
- request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type())
241
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
242
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
177
243
  response: FieldMap | None = self.callback.show_form_entry_dialog(request)
178
244
  if response is None:
179
245
  raise SapioUserCancelledException()
@@ -215,13 +281,13 @@ class CallbackUtil:
215
281
  modifier = FieldModifier(visible=True, editable=editable)
216
282
 
217
283
  # Build the form using only those fields that are desired.
284
+ values: dict[str, Any] = {}
218
285
  builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
219
286
  for field_name in fields:
220
287
  field_def = field_defs.get(field_name)
221
288
  if field_def is None:
222
289
  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)
290
+ values[field_name] = record.get_field_value(field_name)
225
291
  column: int = 0
226
292
  span: int = 4
227
293
  if column_positions and field_name in column_positions:
@@ -230,7 +296,8 @@ class CallbackUtil:
230
296
  span = position[1]
231
297
  builder.add_field(modifier.modify_field(field_def), column, span)
232
298
 
233
- request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type())
299
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
300
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
234
301
  response: FieldMap | None = self.callback.show_form_entry_dialog(request)
235
302
  if response is None:
236
303
  raise SapioUserCancelledException()
@@ -245,7 +312,8 @@ class CallbackUtil:
245
312
  :param field: The definition for a field that the user must provide input to.
246
313
  :return: The response value from the user for the given field.
247
314
  """
248
- request = InputDialogCriteria(title, msg, field, self.width_pixels, self.width_percent)
315
+ request = InputDialogCriteria(title, msg, field,
316
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
249
317
  response: Any | None = self.callback.show_input_dialog(request)
250
318
  if response is None:
251
319
  raise SapioUserCancelledException()
@@ -322,6 +390,8 @@ class CallbackUtil:
322
390
  msg: str,
323
391
  fields: list[AbstractVeloxFieldDefinition],
324
392
  values: list[FieldMap],
393
+ group_by: str | None = None,
394
+ image_data: list[bytes] | None = None,
325
395
  *,
326
396
  data_type: str = "Default",
327
397
  display_name: str | None = None,
@@ -335,6 +405,10 @@ class CallbackUtil:
335
405
  :param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
336
406
  they are provided in this list.
337
407
  :param values: The values to set for each row of the table.
408
+ :param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
409
+ The user may remove this grouping if they want to.
410
+ :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
411
+ the image data list corresponds to the element at the same index in the values list.
338
412
  :param data_type: The data type name for the temporary data type that will be created for this table.
339
413
  :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
340
414
  name.
@@ -356,7 +430,9 @@ class CallbackUtil:
356
430
  for field in fields:
357
431
  builder.add_field(modifier.modify_field(field))
358
432
 
359
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values)
433
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
434
+ record_image_data_list=image_data, group_by_field=group_by,
435
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
360
436
  response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
361
437
  if response is None:
362
438
  raise SapioUserCancelledException()
@@ -367,7 +443,9 @@ class CallbackUtil:
367
443
  msg: str,
368
444
  fields: list[str],
369
445
  records: list[SapioRecord],
370
- editable: bool | None = True) -> list[FieldMap]:
446
+ editable: bool | None = True,
447
+ group_by: str | None = None,
448
+ image_data: list[bytes] | None = None) -> list[FieldMap]:
371
449
  """
372
450
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
373
451
  a given list of records of a singular type. Provided field names must match fields on the definition of the data
@@ -384,6 +462,10 @@ class CallbackUtil:
384
462
  they are provided in this list.
385
463
  :param editable: If true, all fields are displayed as editable. If false, all fields are displayed as
386
464
  uneditable. If none, only those fields that are defined as editable by the data designer will be editable.
465
+ :param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
466
+ The user may remove this grouping if they want to.
467
+ :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
468
+ the image data list corresponds to the element at the same index in the records list.
387
469
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
388
470
  value from the user for that field for each row.
389
471
  """
@@ -410,7 +492,9 @@ class CallbackUtil:
410
492
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
411
493
  builder.add_field(modifier.modify_field(field_def))
412
494
 
413
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list)
495
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list,
496
+ record_image_data_list=image_data, group_by_field=group_by,
497
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
414
498
  response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
415
499
  if response is None:
416
500
  raise SapioUserCancelledException()
@@ -567,7 +651,8 @@ class CallbackUtil:
567
651
  for field in final_fields:
568
652
  builder.add_field(field)
569
653
 
570
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values)
654
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
655
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
571
656
  response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
572
657
  if response is None:
573
658
  raise SapioUserCancelledException()
@@ -616,7 +701,8 @@ class CallbackUtil:
616
701
  raise SapioException(f"The data type \"{data_type}\" does not have a layout by the name "
617
702
  f"\"{layout_name}\" in the system.")
618
703
 
619
- request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list)
704
+ request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list,
705
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
620
706
  response: bool = self.callback.data_record_form_view_dialog(request)
621
707
  if not response:
622
708
  raise SapioUserCancelledException()
@@ -663,7 +749,7 @@ class CallbackUtil:
663
749
  return response
664
750
 
665
751
  def record_selection_dialog(self, msg: str, fields: list[str], records: list[SapioRecord],
666
- multi_select: bool = True) -> list[FieldMap]:
752
+ multi_select: bool = True) -> list[SapioRecord]:
667
753
  """
668
754
  Create a record selection dialog for a list of records for the user to choose from. Provided field names must
669
755
  match fields on the definition of the data type of the given records.
@@ -676,7 +762,7 @@ class CallbackUtil:
676
762
  they are provided in this list.
677
763
  :param records: The records to display as rows in the table.
678
764
  :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.
765
+ :return: A list of the selected records.
680
766
  """
681
767
  data_types: set[str] = {x.data_type_name for x in records}
682
768
  if len(data_types) > 1:
@@ -774,7 +860,7 @@ class CallbackUtil:
774
860
 
775
861
  # If CustomReportCriteria was provided, it must be wrapped as a CustomReport.
776
862
  if isinstance(custom_search, CustomReportCriteria):
777
- custom_search: CustomReport = CustomReport(False, None, custom_search)
863
+ custom_search: CustomReport = CustomReport(False, [], custom_search)
778
864
  # If a string was provided, locate the report criteria for the predefined search in the system matching this
779
865
  # name.
780
866
  if isinstance(custom_search, str):
@@ -807,7 +893,8 @@ class CallbackUtil:
807
893
  for field in additional_fields:
808
894
  builder.add_field(field)
809
895
  temp_dt = builder.get_temporary_data_type()
810
- request = ESigningRequestPojo(title, msg, show_comment, temp_dt)
896
+ request = ESigningRequestPojo(title, msg, show_comment, temp_dt,
897
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
811
898
  response: ESigningResponsePojo | None = self.callback.show_esign_dialog(request)
812
899
  if response is None:
813
900
  raise SapioUserCancelledException()
@@ -892,7 +979,7 @@ class CallbackUtil:
892
979
  if len(allowed_extensions) != 0:
893
980
  matches: bool = False
894
981
  for ext in allowed_extensions:
895
- if file_path.endswith("." + ext):
982
+ if file_path.endswith("." + ext.lstrip(".")):
896
983
  matches = True
897
984
  break
898
985
  if matches is False:
@@ -906,9 +993,19 @@ class CallbackUtil:
906
993
  :param file_name: The name of the file.
907
994
  :param file_data: The data of the file, provided as either a string or as a bytes array.
908
995
  """
909
- data = io.StringIO(file_data) if isinstance(file_data, str) else io.BytesIO(file_data)
996
+ data = io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data)
910
997
  self.callback.send_file(file_name, False, data)
911
998
 
999
+ def write_zip_file(self, zip_name: str, files: dict[str, str | bytes]) -> None:
1000
+ """
1001
+ Send a collection of files to the user in a zip file.
1002
+
1003
+ :param zip_name: The name of the zip file.
1004
+ :param files: A dictionary of the files to add to the zip file.
1005
+ """
1006
+ data = io.BytesIO(FileUtil.zip_files(files))
1007
+ self.callback.send_file(zip_name, False, data)
1008
+
912
1009
 
913
1010
  class FieldModifier:
914
1011
  """
@@ -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)