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.
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/PKG-INFO +1 -1
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/pyproject.toml +1 -1
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/callbacks/callback_util.py +133 -37
- sapiopycommons-2024.8.28a315/src/sapiopycommons/customreport/column_builder.py +60 -0
- sapiopycommons-2024.8.28a315/src/sapiopycommons/customreport/custom_report_builder.py +125 -0
- sapiopycommons-2024.8.28a315/src/sapiopycommons/customreport/term_builder.py +299 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/datatype/attachment_util.py +11 -10
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/eln/experiment_handler.py +209 -48
- sapiopycommons-2024.8.28a315/src/sapiopycommons/eln/experiment_report_util.py +118 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/complex_data_loader.py +5 -4
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/file_bridge.py +15 -14
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/file_bridge_handler.py +27 -5
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/file_data_handler.py +2 -5
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/file_util.py +38 -5
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/file_validator.py +26 -11
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/files/file_writer.py +44 -15
- sapiopycommons-2024.8.28a315/src/sapiopycommons/general/aliases.py +226 -0
- sapiopycommons-2024.8.28a315/src/sapiopycommons/general/audit_log.py +196 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/general/custom_report_util.py +34 -32
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/general/popup_util.py +17 -0
- sapiopycommons-2024.8.28a315/src/sapiopycommons/general/sapio_links.py +50 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/general/time_util.py +40 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/multimodal/multimodal_data.py +0 -1
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/processtracking/endpoints.py +22 -22
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/recordmodel/record_handler.py +228 -77
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/rules/eln_rule_handler.py +34 -25
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/rules/on_save_rule_handler.py +34 -31
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/webhook/webhook_handlers.py +90 -26
- sapiopycommons-2024.8.28a315/src/sapiopycommons/webhook/webservice_handlers.py +67 -0
- sapiopycommons-2024.8.28a315/tests/_do_not_add_init_py_here +0 -0
- sapiopycommons-2024.8.28a314/src/sapiopycommons/eln/experiment_report_util.py +0 -214
- sapiopycommons-2024.8.28a314/src/sapiopycommons/general/aliases.py +0 -82
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/.gitignore +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/LICENSE +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/README.md +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/__init__.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/callbacks/__init__.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/chem/IndigoMolecules.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/chem/Molecules.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/chem/__init__.py +0 -0
- {sapiopycommons-2024.8.28a314/src/sapiopycommons/datatype → sapiopycommons-2024.8.28a315/src/sapiopycommons/customreport}/__init__.py +0 -0
- {sapiopycommons-2024.8.28a314/src/sapiopycommons/eln → sapiopycommons-2024.8.28a315/src/sapiopycommons/datatype}/__init__.py +0 -0
- {sapiopycommons-2024.8.28a314/src/sapiopycommons/files → sapiopycommons-2024.8.28a315/src/sapiopycommons/eln}/__init__.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/eln/plate_designer.py +0 -0
- {sapiopycommons-2024.8.28a314/src/sapiopycommons/general → sapiopycommons-2024.8.28a315/src/sapiopycommons/files}/__init__.py +0 -0
- {sapiopycommons-2024.8.28a314/src/sapiopycommons/processtracking → sapiopycommons-2024.8.28a315/src/sapiopycommons/general}/__init__.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/general/accession_service.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/general/exceptions.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/general/storage_util.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/src/sapiopycommons/multimodal/multimodal.py +0 -0
- {sapiopycommons-2024.8.28a314/src/sapiopycommons/recordmodel → sapiopycommons-2024.8.28a315/src/sapiopycommons/processtracking}/__init__.py +0 -0
- {sapiopycommons-2024.8.28a314/src/sapiopycommons/rules → sapiopycommons-2024.8.28a315/src/sapiopycommons/recordmodel}/__init__.py +0 -0
- {sapiopycommons-2024.8.28a314/src/sapiopycommons/webhook → sapiopycommons-2024.8.28a315/src/sapiopycommons/rules}/__init__.py +0 -0
- /sapiopycommons-2024.8.28a314/tests/_do_not_add_init_py_here → /sapiopycommons-2024.8.28a315/src/sapiopycommons/webhook/__init__.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/tests/accession_test.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/tests/bio_reg_test.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/tests/chem_test.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/tests/data_type_models.py +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/tests/kappa.chains.fasta +0 -0
- {sapiopycommons-2024.8.28a314 → sapiopycommons-2024.8.28a315}/tests/mafft_test.py +0 -0
- {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.
|
|
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>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
|
-
from
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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) ->
|
|
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,
|
|
249
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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[
|
|
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
|
|
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,
|
|
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) ->
|
|
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.
|
|
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)
|