sapiopycommons 2025.4.8a474__py3-none-any.whl → 2025.4.9a150__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sapiopycommons might be problematic. Click here for more details.
- sapiopycommons/callbacks/callback_util.py +392 -1262
- sapiopycommons/callbacks/field_builder.py +0 -2
- sapiopycommons/chem/Molecules.py +2 -0
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/attachment_util.py +2 -4
- sapiopycommons/datatype/data_fields.py +1 -23
- sapiopycommons/eln/experiment_handler.py +279 -933
- sapiopycommons/eln/experiment_report_util.py +10 -15
- sapiopycommons/eln/plate_designer.py +59 -159
- sapiopycommons/files/file_bridge.py +0 -76
- sapiopycommons/files/file_bridge_handler.py +110 -325
- sapiopycommons/files/file_data_handler.py +2 -2
- sapiopycommons/files/file_util.py +15 -40
- sapiopycommons/files/file_validator.py +5 -6
- sapiopycommons/files/file_writer.py +1 -1
- sapiopycommons/flowcyto/flow_cyto.py +1 -1
- sapiopycommons/general/accession_service.py +3 -3
- sapiopycommons/general/aliases.py +28 -51
- sapiopycommons/general/audit_log.py +2 -2
- sapiopycommons/general/custom_report_util.py +1 -24
- sapiopycommons/general/exceptions.py +2 -41
- sapiopycommons/general/popup_util.py +2 -2
- sapiopycommons/multimodal/multimodal.py +0 -1
- sapiopycommons/processtracking/custom_workflow_handler.py +30 -46
- sapiopycommons/recordmodel/record_handler.py +159 -547
- sapiopycommons/rules/eln_rule_handler.py +30 -41
- sapiopycommons/rules/on_save_rule_handler.py +30 -41
- sapiopycommons/webhook/webhook_handlers.py +55 -448
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.4.8a474.dist-info → sapiopycommons-2025.4.9a150.dist-info}/METADATA +1 -1
- sapiopycommons-2025.4.9a150.dist-info/RECORD +59 -0
- sapiopycommons/customreport/auto_pagers.py +0 -281
- sapiopycommons/eln/experiment_cache.py +0 -173
- sapiopycommons/eln/experiment_step_factory.py +0 -474
- sapiopycommons/eln/experiment_tags.py +0 -7
- sapiopycommons/eln/step_creation.py +0 -235
- sapiopycommons/general/data_structure_util.py +0 -115
- sapiopycommons/general/directive_util.py +0 -86
- sapiopycommons/samples/aliquot.py +0 -48
- sapiopycommons-2025.4.8a474.dist-info/RECORD +0 -67
- {sapiopycommons-2025.4.8a474.dist-info → sapiopycommons-2025.4.9a150.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.4.8a474.dist-info → sapiopycommons-2025.4.9a150.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,49 +1,28 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import sys
|
|
5
|
-
import time
|
|
6
1
|
import traceback
|
|
7
2
|
from abc import abstractmethod
|
|
8
3
|
from logging import Logger
|
|
9
4
|
|
|
10
|
-
from sapiopylib.rest import UserManagerService, GroupManagerService, MessengerService
|
|
11
|
-
from sapiopylib.rest.AccessionService import AccessionManager
|
|
12
|
-
from sapiopylib.rest.CustomReportService import CustomReportManager
|
|
13
|
-
from sapiopylib.rest.DashboardManager import DashboardManager
|
|
14
5
|
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
15
6
|
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
16
|
-
from sapiopylib.rest.DataService import DataManager
|
|
17
|
-
from sapiopylib.rest.DataTypeService import DataTypeManager
|
|
18
|
-
from sapiopylib.rest.ELNService import ElnManager
|
|
19
|
-
from sapiopylib.rest.PicklistService import PickListManager
|
|
20
|
-
from sapiopylib.rest.ReportManager import ReportManager
|
|
21
|
-
from sapiopylib.rest.SesssionManagerService import SessionManager
|
|
22
7
|
from sapiopylib.rest.User import SapioUser
|
|
23
8
|
from sapiopylib.rest.WebhookService import AbstractWebhookHandler
|
|
24
9
|
from sapiopylib.rest.pojo.Message import VeloxLogMessage, VeloxLogLevel
|
|
25
|
-
from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import PopupType
|
|
26
10
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
27
11
|
from sapiopylib.rest.pojo.webhook.WebhookEnums import WebhookEndpointType
|
|
28
12
|
from sapiopylib.rest.pojo.webhook.WebhookResult import SapioWebhookResult
|
|
29
13
|
from sapiopylib.rest.utils.DataTypeCacheManager import DataTypeCacheManager
|
|
30
|
-
from sapiopylib.rest.utils.FoundationAccessioning import FoundationAccessionManager
|
|
31
14
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager, \
|
|
32
15
|
RecordModelRelationshipManager
|
|
33
16
|
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
34
|
-
from sapiopylib.rest.utils.recordmodel.last_saved import LastSavedValueManager
|
|
35
17
|
|
|
36
18
|
from sapiopycommons.callbacks.callback_util import CallbackUtil
|
|
37
19
|
from sapiopycommons.eln.experiment_handler import ExperimentHandler
|
|
38
|
-
from sapiopycommons.general.directive_util import DirectiveUtil
|
|
39
20
|
from sapiopycommons.general.exceptions import SapioUserErrorException, SapioCriticalErrorException, \
|
|
40
|
-
SapioUserCancelledException, SapioException, SapioDialogTimeoutException
|
|
21
|
+
SapioUserCancelledException, SapioException, SapioDialogTimeoutException
|
|
41
22
|
from sapiopycommons.general.sapio_links import SapioNavigationLinker
|
|
42
|
-
from sapiopycommons.general.time_util import TimeUtil
|
|
43
23
|
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
44
24
|
from sapiopycommons.rules.eln_rule_handler import ElnRuleHandler
|
|
45
25
|
from sapiopycommons.rules.on_save_rule_handler import OnSaveRuleHandler
|
|
46
|
-
from sapiopycommons.webhook.webhook_context import ProcessQueueContext
|
|
47
26
|
|
|
48
27
|
|
|
49
28
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
@@ -55,156 +34,37 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
55
34
|
"""
|
|
56
35
|
logger: Logger
|
|
57
36
|
|
|
58
|
-
_start_time: float
|
|
59
|
-
_start_time_epoch: int
|
|
60
|
-
|
|
61
37
|
user: SapioUser
|
|
62
|
-
"""The user who invoked this webhook. Used for authenticating requests back to the Sapio server."""
|
|
63
|
-
group_name: str
|
|
64
|
-
"""The name of the group that the user invoked this webhook with."""
|
|
65
|
-
user_utc_offset_seconds: int
|
|
66
|
-
"""The number of seconds that the user is offset from the UTC timezone. Able to be used with TimeUtil to display
|
|
67
|
-
timestamps in the user's timezone."""
|
|
68
38
|
context: SapioWebhookContext
|
|
69
|
-
"""The context from the server of this webhook's invocation."""
|
|
70
|
-
|
|
71
|
-
# CR-47383: Include every manager from DataMgmtServer for easier access.
|
|
72
|
-
acc_man: AccessionManager
|
|
73
|
-
"""A class for making requests to the accession webservice endpoints."""
|
|
74
|
-
fnd_acc_man: FoundationAccessionManager
|
|
75
|
-
"""A class for making requests to the Foundations accession webservice endpoints."""
|
|
76
|
-
report_man: CustomReportManager
|
|
77
|
-
"""A class for making requests to the custom report webservice endpoints."""
|
|
78
|
-
dash_man: DashboardManager
|
|
79
|
-
"""A class for making requests to the dashboard management webservice endpoints."""
|
|
80
|
-
xml_data_man: DataManager
|
|
81
|
-
"""A class for making requests to the data record import/export via XML webservice endpoints."""
|
|
82
|
-
dr_man: DataRecordManager
|
|
83
|
-
"""A class for making requests to the data record webservice endpoints."""
|
|
84
|
-
dt_man: DataTypeManager
|
|
85
|
-
"""A class for making requests to the data type webservice endpoints."""
|
|
86
|
-
eln_man: ElnManager
|
|
87
|
-
"""A class for making requests to the ELN management webservice endpoints."""
|
|
88
|
-
group_man: GroupManagerService
|
|
89
|
-
"""A class for making requests to the group management webservice endpoints."""
|
|
90
|
-
messenger: MessengerService
|
|
91
|
-
"""A class for making requests to the message webservice endpoints."""
|
|
92
|
-
list_man: PickListManager
|
|
93
|
-
"""A class for making requests to the pick list webservice endpoints."""
|
|
94
|
-
pdf_report_man: ReportManager
|
|
95
|
-
"""A class for making requests to the report webservice endpoints."""
|
|
96
|
-
session_man: SessionManager
|
|
97
|
-
"""A class for making requests to the session management webservice endpoints."""
|
|
98
|
-
user_man: UserManagerService
|
|
99
|
-
"""A class for making requests to the user management webservice endpoints."""
|
|
100
39
|
|
|
40
|
+
dr_man: DataRecordManager
|
|
101
41
|
rec_man: RecordModelManager
|
|
102
|
-
"""The record model manager. Used for committing record model changes to the system."""
|
|
103
42
|
inst_man: RecordModelInstanceManager
|
|
104
|
-
"""The record model instance manager. Used for adding record models to the cache."""
|
|
105
43
|
rel_man: RecordModelRelationshipManager
|
|
106
|
-
"""The record model relationship manager. Used for loading parent/child and side-link relationships between record
|
|
107
|
-
models."""
|
|
108
44
|
# FR-46329: Add the ancestor manager to CommonsWebhookHandler.
|
|
109
45
|
an_man: RecordModelAncestorManager
|
|
110
|
-
"""The record model ancestor manager. Used for loading ancestor relationships between record models."""
|
|
111
|
-
saved_vals_man: LastSavedValueManager
|
|
112
|
-
"""The last saved values manager. Used for determining what the record values were prior to this commit."""
|
|
113
46
|
|
|
114
47
|
dt_cache: DataTypeCacheManager
|
|
115
|
-
"""A class that calls the same endpoints as the DataTypeManager (self.dt_man), except the results are cached so that
|
|
116
|
-
repeated calls to the same function don't result in duplicate webservice calls. """
|
|
117
48
|
rec_handler: RecordHandler
|
|
118
|
-
"""A class that behaves like a combination between the DataRecordManager and RecordModelInstanceManager, allowing
|
|
119
|
-
you to query and wrap record as record models in a single function call, among other functions useful for dealing
|
|
120
|
-
with record models."""
|
|
121
49
|
callback: CallbackUtil
|
|
122
|
-
"""A class for making requests to the client callback webservice endpoints."""
|
|
123
|
-
directive: DirectiveUtil
|
|
124
|
-
"""A class for making directives that redirect the user to a new webpage after this webhook returns a result."""
|
|
125
50
|
exp_handler: ExperimentHandler | None
|
|
126
|
-
"""If this webhook was invoked from within an ELN experiment, this variable will be populated with an
|
|
127
|
-
ExperimentHandler initialized from the context. """
|
|
128
51
|
rule_handler: OnSaveRuleHandler | ElnRuleHandler | None
|
|
129
|
-
"""If this is an ELN or on save rule endpoint, this variable will be populated with an ElnRuleHandler or
|
|
130
|
-
OnSaveRuleHandler, depending on the endpoint type."""
|
|
131
|
-
custom_context: ProcessQueueContext | None
|
|
132
|
-
"""If this is a custom endpoint, this variable will be populated with an object that parses the custom context
|
|
133
|
-
data."""
|
|
134
|
-
|
|
135
|
-
# FR-47390: Allow for classes that extend CommonsWebhookHandler to change how exception messages are displayed
|
|
136
|
-
# to the user be changing these variables instead of needing to override the exception handling functions.
|
|
137
|
-
default_user_error_display_type: MessageDisplayType
|
|
138
|
-
"""The default message display type for user error exceptions. If a user error exception is thrown and doesn't
|
|
139
|
-
specify a display type, this type will be used."""
|
|
140
|
-
default_critical_error_display_type: MessageDisplayType
|
|
141
|
-
"""The default message display type for critical error exceptions. If a critical error exception is thrown and
|
|
142
|
-
doesn't specify a display type, this type will be used."""
|
|
143
|
-
default_dialog_timeout_display_type: MessageDisplayType
|
|
144
|
-
"""The default message display type for dialog timeout exceptions."""
|
|
145
|
-
default_unexpected_error_display_type: MessageDisplayType
|
|
146
|
-
"""The default message display type for unexpected exceptions."""
|
|
147
|
-
|
|
148
|
-
default_user_error_title: str
|
|
149
|
-
"""The default title to display to the user when a user error occurs. If a user error exception is thrown and
|
|
150
|
-
doesn't specify a title, this title will be used."""
|
|
151
|
-
default_critical_error_title: str
|
|
152
|
-
"""The default title to display to the user when a critical error occurs. If a critical error exception is thrown
|
|
153
|
-
and doesn't specify a title, this title will be used."""
|
|
154
|
-
default_dialog_timeout_title: str
|
|
155
|
-
"""The default title to display to the user when a dialog times out."""
|
|
156
|
-
default_unexpected_error_title: str
|
|
157
|
-
"""The default title to display to the user when an unexpected exception occurs."""
|
|
158
|
-
|
|
159
|
-
default_dialog_timeout_message: str
|
|
160
|
-
"""The default message to display to the user when a dialog times out."""
|
|
161
|
-
default_unexpected_error_message: str
|
|
162
|
-
"""The default message to display to the user when an unexpected exception occurs."""
|
|
163
52
|
|
|
164
53
|
def run(self, context: SapioWebhookContext) -> SapioWebhookResult:
|
|
165
|
-
# Timestamps used for measuring performance.
|
|
166
|
-
self._start_time = time.perf_counter()
|
|
167
|
-
self._start_time_epoch = TimeUtil.now_in_millis()
|
|
168
|
-
|
|
169
|
-
# Save the webhook context so that any function of this class can access it.
|
|
170
|
-
self.context = context
|
|
171
|
-
|
|
172
|
-
# Save the user and commonly sought after user information.
|
|
173
54
|
self.user = context.user
|
|
174
|
-
self.
|
|
175
|
-
self.user_utc_offset_seconds = self.user.session_additional_data.utc_offset_seconds
|
|
55
|
+
self.context = context
|
|
176
56
|
|
|
177
|
-
# Get the logger from the user.
|
|
178
57
|
self.logger = self.user.logger
|
|
179
58
|
|
|
180
|
-
# Initialize basic manager classes from sapiopylib.
|
|
181
|
-
self.acc_man = DataMgmtServer.get_accession_manager(self.user)
|
|
182
|
-
self.report_man = DataMgmtServer.get_custom_report_manager(self.user)
|
|
183
|
-
self.dash_man = DataMgmtServer.get_dashboard_manager(self.user)
|
|
184
|
-
self.xml_data_man = DataMgmtServer.get_data_manager(self.user)
|
|
185
59
|
self.dr_man = context.data_record_manager
|
|
186
|
-
self.dt_man = DataMgmtServer.get_data_type_manager(self.user)
|
|
187
|
-
self.eln_man = context.eln_manager
|
|
188
|
-
self.group_man = DataMgmtServer.get_group_manager(self.user)
|
|
189
|
-
self.messenger = DataMgmtServer.get_messenger(self.user)
|
|
190
|
-
self.list_man = DataMgmtServer.get_picklist_manager(self.user)
|
|
191
|
-
self.pdf_report_man = DataMgmtServer.get_report_manager(self.user)
|
|
192
|
-
self.session_man = DataMgmtServer.get_session_manager(self.user)
|
|
193
|
-
self.user_man = DataMgmtServer.get_user_manager(self.user)
|
|
194
|
-
|
|
195
|
-
# Initialize record model managers.
|
|
196
60
|
self.rec_man = RecordModelManager(self.user)
|
|
197
61
|
self.inst_man = self.rec_man.instance_manager
|
|
198
62
|
self.rel_man = self.rec_man.relationship_manager
|
|
199
|
-
self.an_man = self.rec_man
|
|
200
|
-
self.saved_vals_man = self.rec_man.last_saved_manager
|
|
63
|
+
self.an_man = RecordModelAncestorManager(self.rec_man)
|
|
201
64
|
|
|
202
|
-
# Initialize more complex classes from sapiopylib and sapiopycommons.
|
|
203
|
-
self.fnd_acc_man = FoundationAccessionManager(self.user)
|
|
204
65
|
self.dt_cache = DataTypeCacheManager(self.user)
|
|
205
66
|
self.rec_handler = RecordHandler(context)
|
|
206
67
|
self.callback = CallbackUtil(context)
|
|
207
|
-
self.directive = DirectiveUtil(context)
|
|
208
68
|
if context.eln_experiment is not None:
|
|
209
69
|
self.exp_handler = ExperimentHandler(context)
|
|
210
70
|
else:
|
|
@@ -215,29 +75,6 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
215
75
|
self.rule_handler = ElnRuleHandler(context)
|
|
216
76
|
else:
|
|
217
77
|
self.rule_handler = None
|
|
218
|
-
if self.is_custom():
|
|
219
|
-
self.custom_context = ProcessQueueContext(context)
|
|
220
|
-
else:
|
|
221
|
-
self.custom_context = None
|
|
222
|
-
|
|
223
|
-
# CR-47526: Set the dialog timeout to 1 hour by default. This can be overridden by the webhook.
|
|
224
|
-
self.callback.set_dialog_timeout(3600)
|
|
225
|
-
|
|
226
|
-
# Set the default display types, titles, and messages for each type of exception that can display a message.
|
|
227
|
-
self.default_user_error_display_type = MessageDisplayType.TOASTER_WARNING
|
|
228
|
-
self.default_critical_error_display_type = MessageDisplayType.DISPLAY_ERROR
|
|
229
|
-
self.default_dialog_timeout_display_type = MessageDisplayType.OK_DIALOG
|
|
230
|
-
self.default_unexpected_error_display_type = MessageDisplayType.TOASTER_WARNING
|
|
231
|
-
|
|
232
|
-
self.default_user_error_title = ""
|
|
233
|
-
self.default_critical_error_title = ""
|
|
234
|
-
self.default_dialog_timeout_title = "Dialog Timeout"
|
|
235
|
-
self.default_unexpected_error_title = ""
|
|
236
|
-
|
|
237
|
-
self.default_dialog_timeout_message = ("You have remained idle for too long and this dialog has timed out. "
|
|
238
|
-
"Close and re-initiate it to continue.")
|
|
239
|
-
self.default_unexpected_error_message = ("Unexpected error occurred during webhook execution. Please contact "
|
|
240
|
-
"Sapio support.")
|
|
241
78
|
|
|
242
79
|
# Wrap the execution of each webhook in a try/catch. If an exception occurs, handle any special sapiopycommons
|
|
243
80
|
# exceptions. Otherwise, return a generic message stating that an error occurred.
|
|
@@ -287,18 +124,7 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
287
124
|
if result is not None:
|
|
288
125
|
return result
|
|
289
126
|
self.log_error(traceback.format_exc())
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
display_type: MessageDisplayType = e.display_type if e.display_type else self.default_user_error_display_type
|
|
293
|
-
title: str = e.title if e.title is not None else self.default_user_error_title
|
|
294
|
-
return self._display_exception(e.msg, display_type, title)
|
|
295
|
-
|
|
296
|
-
def handle_user_error_exception_extra(self, e: SapioUserErrorException) -> None:
|
|
297
|
-
"""
|
|
298
|
-
An additional function that can be overridden to provide extra behavior when a SapioUserErrorException is thrown.
|
|
299
|
-
Default behavior does nothing.
|
|
300
|
-
"""
|
|
301
|
-
pass
|
|
127
|
+
return SapioWebhookResult(False, display_text=e.args[0])
|
|
302
128
|
|
|
303
129
|
def handle_critical_error_exception(self, e: SapioCriticalErrorException) -> SapioWebhookResult:
|
|
304
130
|
"""
|
|
@@ -313,18 +139,13 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
313
139
|
if result is not None:
|
|
314
140
|
return result
|
|
315
141
|
self.log_error(traceback.format_exc())
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
"""
|
|
324
|
-
An additional function that can be overridden to provide extra behavior when a SapioCriticalErrorException is
|
|
325
|
-
thrown. Default behavior does nothing.
|
|
326
|
-
"""
|
|
327
|
-
pass
|
|
142
|
+
# This error can be thrown by endpoints that can't send client callbacks. If that happens, fall back onto
|
|
143
|
+
# sending display text instead.
|
|
144
|
+
if self.can_send_client_callback():
|
|
145
|
+
self.callback.display_error(e.args[0])
|
|
146
|
+
else:
|
|
147
|
+
return SapioWebhookResult(False, e.args[0])
|
|
148
|
+
return SapioWebhookResult(False)
|
|
328
149
|
|
|
329
150
|
def handle_user_cancelled_exception(self, e: SapioUserCancelledException) -> SapioWebhookResult:
|
|
330
151
|
"""
|
|
@@ -339,17 +160,7 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
339
160
|
result: SapioWebhookResult | None = self.handle_any_exception(e)
|
|
340
161
|
if result is not None:
|
|
341
162
|
return result
|
|
342
|
-
|
|
343
|
-
# FR-47390: Return a False result for user cancelled exceptions so that transactional webhooks cancel the
|
|
344
|
-
# commit.
|
|
345
|
-
return SapioWebhookResult(False)
|
|
346
|
-
|
|
347
|
-
def handle_user_cancelled_exception_extra(self, e: SapioUserCancelledException) -> None:
|
|
348
|
-
"""
|
|
349
|
-
An additional function that can be overridden to provide extra behavior when a SapioUserCancelledException is
|
|
350
|
-
thrown. Default behavior does nothing.
|
|
351
|
-
"""
|
|
352
|
-
pass
|
|
163
|
+
return SapioWebhookResult(True)
|
|
353
164
|
|
|
354
165
|
def handle_dialog_timeout_exception(self, e: SapioDialogTimeoutException) -> SapioWebhookResult:
|
|
355
166
|
"""
|
|
@@ -364,17 +175,15 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
364
175
|
result: SapioWebhookResult | None = self.handle_any_exception(e)
|
|
365
176
|
if result is not None:
|
|
366
177
|
return result
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
"""
|
|
377
|
-
pass
|
|
178
|
+
# This dialog could time out too! Ignore it if it does.
|
|
179
|
+
# No need to check can_send_client_callback() here, as this exception should only be thrown by endpoints that
|
|
180
|
+
# are capable of sending callbacks.
|
|
181
|
+
try:
|
|
182
|
+
self.callback.ok_dialog("Notice", "You have remained idle for too long and this dialog has timed out. "
|
|
183
|
+
"Close and re-initiate it to continue.")
|
|
184
|
+
except SapioDialogTimeoutException:
|
|
185
|
+
pass
|
|
186
|
+
return SapioWebhookResult(False)
|
|
378
187
|
|
|
379
188
|
def handle_unexpected_exception(self, e: Exception) -> SapioWebhookResult:
|
|
380
189
|
"""
|
|
@@ -391,21 +200,13 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
391
200
|
if result is not None:
|
|
392
201
|
return result
|
|
393
202
|
msg: str = traceback.format_exc()
|
|
394
|
-
self.log_error(msg
|
|
203
|
+
self.log_error(msg)
|
|
395
204
|
# FR-47079: Also log all unexpected exception messages to the webhook execution log within the platform.
|
|
396
|
-
self.log_error_to_webhook_execution_log(msg
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
self.default_unexpected_error_display_type,
|
|
400
|
-
self.default_unexpected_error_title)
|
|
401
|
-
|
|
402
|
-
def handle_unexpected_exception_extra(self, e: Exception) -> None:
|
|
403
|
-
"""
|
|
404
|
-
An additional function that can be overridden to provide extra behavior when a generic exception is thrown.
|
|
405
|
-
Default behavior does nothing.
|
|
406
|
-
"""
|
|
407
|
-
pass
|
|
205
|
+
self.log_error_to_webhook_execution_log(msg)
|
|
206
|
+
return SapioWebhookResult(False, display_text="Unexpected error occurred during webhook execution. "
|
|
207
|
+
"Please contact Sapio support.")
|
|
408
208
|
|
|
209
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
409
210
|
def handle_any_exception(self, e: Exception) -> SapioWebhookResult | None:
|
|
410
211
|
"""
|
|
411
212
|
An exception handler which runs regardless of the type of exception that was raised. Can be used to "rollback"
|
|
@@ -416,55 +217,7 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
416
217
|
sent by one of the normal exception handlers, or may return None if no result needs returned. If a result is
|
|
417
218
|
returned, then the default behavior of other exception handlers is skipped.
|
|
418
219
|
"""
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
def display_message(self, msg: str, display_type: MessageDisplayType, title: str = "") -> bool:
|
|
422
|
-
"""
|
|
423
|
-
Display a message to the user. The form that the message takes depends on the display type.
|
|
424
|
-
|
|
425
|
-
:param msg: The message to display to the user.
|
|
426
|
-
:param display_type: The manner in which the message should be displayed.
|
|
427
|
-
:param title: If the display type is able to have a title, this is the title that will be displayed.
|
|
428
|
-
:return: True if the message was displayed. False if the message could not be displayed (because this
|
|
429
|
-
webhook can't send client callbacks).
|
|
430
|
-
"""
|
|
431
|
-
if not self.can_send_client_callback():
|
|
432
|
-
return False
|
|
433
|
-
if display_type == MessageDisplayType.TOASTER_SUCCESS:
|
|
434
|
-
self.callback.toaster_popup(msg, title, PopupType.Success)
|
|
435
|
-
elif display_type == MessageDisplayType.TOASTER_INFO:
|
|
436
|
-
self.callback.toaster_popup(msg, title, PopupType.Info)
|
|
437
|
-
elif display_type == MessageDisplayType.TOASTER_WARNING:
|
|
438
|
-
self.callback.toaster_popup(msg, title, PopupType.Warning)
|
|
439
|
-
elif display_type == MessageDisplayType.TOASTER_ERROR:
|
|
440
|
-
self.callback.toaster_popup(msg, title, PopupType.Error)
|
|
441
|
-
elif display_type == MessageDisplayType.OK_DIALOG:
|
|
442
|
-
self.callback.ok_dialog(title, msg)
|
|
443
|
-
elif display_type == MessageDisplayType.DISPLAY_INFO:
|
|
444
|
-
self.callback.display_info(msg)
|
|
445
|
-
elif display_type == MessageDisplayType.DISPLAY_WARNING:
|
|
446
|
-
self.callback.display_warning(msg)
|
|
447
|
-
elif display_type == MessageDisplayType.DISPLAY_ERROR:
|
|
448
|
-
self.callback.display_error(msg)
|
|
449
|
-
return True
|
|
450
|
-
|
|
451
|
-
def _display_exception(self, msg: str, display_type: MessageDisplayType, title: str) -> SapioWebhookResult:
|
|
452
|
-
"""
|
|
453
|
-
Display an exception message to the user and return a webhook result to end the webhook invocation.
|
|
454
|
-
This handles the cases where the webhook invocation type is incapable of sending client callbacks and must
|
|
455
|
-
instead return the message in the webhook result, and the case where the display type is an OK dialog, which
|
|
456
|
-
may potentially cause a dialog timeout exception.
|
|
457
|
-
"""
|
|
458
|
-
# If the display type is an OK dialog, then we need to handle the dialog timeout exception that could be thrown.
|
|
459
|
-
try:
|
|
460
|
-
# Set the dialog timeout to something low as to not hog the connection.
|
|
461
|
-
self.callback.set_dialog_timeout(60)
|
|
462
|
-
# If this invocation type can't send client callbacks, fallback to sending the message in the result.
|
|
463
|
-
if self.display_message(msg, display_type, title):
|
|
464
|
-
return SapioWebhookResult(False)
|
|
465
|
-
return SapioWebhookResult(False, display_text=msg)
|
|
466
|
-
except SapioDialogTimeoutException:
|
|
467
|
-
return SapioWebhookResult(False)
|
|
220
|
+
return None
|
|
468
221
|
|
|
469
222
|
def log_info(self, msg: str) -> None:
|
|
470
223
|
"""
|
|
@@ -474,69 +227,55 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
474
227
|
"""
|
|
475
228
|
self.logger.info(self._format_log(msg, "log_info call"))
|
|
476
229
|
|
|
477
|
-
def log_error(self, msg: str
|
|
230
|
+
def log_error(self, msg: str) -> None:
|
|
478
231
|
"""
|
|
479
232
|
Write an info message to the webhook server log. Log destination is stdout. This message will include
|
|
480
233
|
information about the user's group, their location in the system, the webhook invocation type, and other
|
|
481
234
|
important information that can be gathered from the context that is useful for debugging.
|
|
482
235
|
"""
|
|
483
236
|
# PR-46209: Use logger.error instead of logger.info when logging errors.
|
|
484
|
-
self.logger.error(self._format_log(msg, "log_error call"
|
|
237
|
+
self.logger.error(self._format_log(msg, "log_error call"))
|
|
485
238
|
|
|
486
|
-
def log_error_to_webhook_execution_log(self, msg: str
|
|
239
|
+
def log_error_to_webhook_execution_log(self, msg: str) -> None:
|
|
487
240
|
"""
|
|
488
241
|
Write an error message to the platform's webhook execution log. This can be reviewed by navigating to the
|
|
489
242
|
webhook configuration where the webhook that called this function is defined and clicking the "View Log"
|
|
490
243
|
button. From there, select one of the rows for the webhook executions and click "Download Log" from the right
|
|
491
244
|
side table.
|
|
492
245
|
"""
|
|
493
|
-
|
|
494
|
-
|
|
246
|
+
messenger = DataMgmtServer.get_messenger(self.user)
|
|
247
|
+
messenger.log_message(VeloxLogMessage(message=self._format_log(msg, "Error occurred during webhook execution."),
|
|
248
|
+
log_level=VeloxLogLevel.ERROR,
|
|
249
|
+
originating_class=self.__class__.__name__))
|
|
495
250
|
|
|
496
|
-
def _format_log(self, msg: str, prefix: str | None = None
|
|
251
|
+
def _format_log(self, msg: str, prefix: str | None = None) -> str:
|
|
497
252
|
"""
|
|
498
253
|
Given a message to log, populate it with some metadata about this particular webhook execution, including
|
|
499
254
|
the group of the user and the invocation type of the webhook call.
|
|
500
255
|
"""
|
|
501
|
-
#
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
256
|
+
# If we're able to, provide a link to the location that the error occurred at.
|
|
257
|
+
navigator = SapioNavigationLinker(self.context)
|
|
258
|
+
if self.context.eln_experiment is not None:
|
|
259
|
+
link = navigator.experiment(self.context.eln_experiment)
|
|
260
|
+
elif self.context.data_record and not self.context.data_record_list:
|
|
261
|
+
link = navigator.data_record(self.context.data_record)
|
|
262
|
+
elif self.context.base_data_record:
|
|
263
|
+
link = navigator.data_record(self.context.base_data_record)
|
|
264
|
+
else:
|
|
265
|
+
link = None
|
|
506
266
|
|
|
507
|
-
|
|
267
|
+
message: str = ""
|
|
268
|
+
if prefix:
|
|
269
|
+
message += prefix + "\n"
|
|
270
|
+
message += f"Webhook invocation type: {self.context.end_point_type.display_name}\n"
|
|
271
|
+
message += f"Username: {self.user.username}\n"
|
|
272
|
+
# CR-46333: Add the user's group to the logging message.
|
|
273
|
+
message += f"User group: {self.user.session_additional_data.current_group_name}\n"
|
|
274
|
+
if link:
|
|
275
|
+
message += f"User location: {link}\n"
|
|
508
276
|
message += msg
|
|
509
277
|
return message
|
|
510
278
|
|
|
511
|
-
@property
|
|
512
|
-
def start_time(self) -> float:
|
|
513
|
-
"""
|
|
514
|
-
:return: The time that this webhook was invoked, represented in seconds. This time comes from a performance
|
|
515
|
-
counter and is not guaranteed to correspond to a date. Only use in comparison to other performance counters.
|
|
516
|
-
"""
|
|
517
|
-
return self._start_time
|
|
518
|
-
|
|
519
|
-
@property
|
|
520
|
-
def start_time_millis(self) -> int:
|
|
521
|
-
"""
|
|
522
|
-
:return: The epoch timestamp in milliseconds for the time that this webhook was invoked.
|
|
523
|
-
"""
|
|
524
|
-
return self._start_time_epoch
|
|
525
|
-
|
|
526
|
-
def elapsed_time(self) -> float:
|
|
527
|
-
"""
|
|
528
|
-
:return: The number of seconds that have elapsed since this webhook was invoked to the time that this function
|
|
529
|
-
is called. Measures using a performance counter to a high degree of accuracy.
|
|
530
|
-
"""
|
|
531
|
-
return time.perf_counter() - self._start_time
|
|
532
|
-
|
|
533
|
-
def elapsed_time_millis(self) -> int:
|
|
534
|
-
"""
|
|
535
|
-
:return: The number of milliseconds that have elapsed since this webhook was invoked to the time that this
|
|
536
|
-
function is called.
|
|
537
|
-
"""
|
|
538
|
-
return TimeUtil.now_in_millis() - self._start_time_epoch
|
|
539
|
-
|
|
540
279
|
def is_main_toolbar(self) -> bool:
|
|
541
280
|
"""
|
|
542
281
|
:return: True if this endpoint was invoked as a main toolbar button.
|
|
@@ -659,135 +398,3 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
659
398
|
:return: Whether client callbacks and directives can be sent from this webhook's endpoint type.
|
|
660
399
|
"""
|
|
661
400
|
return self.context.is_client_callback_available
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
# FR-47390: Move the gathering of webhook information out of log_error_to_webhook_execution_log and into its own class.
|
|
665
|
-
class WebhookStateSummary:
|
|
666
|
-
"""
|
|
667
|
-
A class that summarizes the state of a webhook at the time that it is created. This class is useful for logging
|
|
668
|
-
information about the webhook invocation to the execution log.
|
|
669
|
-
"""
|
|
670
|
-
username: str
|
|
671
|
-
"""The username of the user who invoked the webhook."""
|
|
672
|
-
user_group: str
|
|
673
|
-
"""The group that the user is currently in."""
|
|
674
|
-
start_timestamp: int
|
|
675
|
-
"""The epoch timestamp in milliseconds for when the webhook was invoked."""
|
|
676
|
-
start_utc_time: str
|
|
677
|
-
"""The time that the webhook was invoked in UTC."""
|
|
678
|
-
start_server_time: str | None
|
|
679
|
-
"""The time that the webhook was invoked on the server, if the TimeUtil class has a default timezone set."""
|
|
680
|
-
start_user_time: str
|
|
681
|
-
"""The time that the webhook was invoked for the user, adjusted for their timezone."""
|
|
682
|
-
timestamp: int
|
|
683
|
-
"""The current epoch timestamp in milliseconds."""
|
|
684
|
-
utc_time: str
|
|
685
|
-
"""The current time in UTC."""
|
|
686
|
-
server_time: str | None
|
|
687
|
-
"""The current time on the webhook server, if the TimeUtil class has a default timezone set."""
|
|
688
|
-
user_time: str
|
|
689
|
-
"""The current time for the user, adjusted for their timezone."""
|
|
690
|
-
invocation_type: str
|
|
691
|
-
"""The type of endpoint that this webhook was invoked from."""
|
|
692
|
-
class_name: str
|
|
693
|
-
"""The name of the class that this webhook is an instance of."""
|
|
694
|
-
link: str | None
|
|
695
|
-
"""A link to the location that the webhook was invoked from, if applicable."""
|
|
696
|
-
exc_summary: str | None
|
|
697
|
-
"""A summary of the exception that occurred, if this summary is being created for an exception."""
|
|
698
|
-
|
|
699
|
-
def __init__(self, webhook: CommonsWebhookHandler, summarize_exception: bool = False):
|
|
700
|
-
"""
|
|
701
|
-
:param webhook: The webhook that this summary is being created for.
|
|
702
|
-
:param summarize_exception: If true, then this summary will include information about the most recent exception
|
|
703
|
-
that occurred during the execution of the webhook.
|
|
704
|
-
"""
|
|
705
|
-
# User information.
|
|
706
|
-
self.username = webhook.user.username
|
|
707
|
-
self.user_group = webhook.group_name
|
|
708
|
-
|
|
709
|
-
# Time information.
|
|
710
|
-
fmt: str = "%Y-%m-%d %H:%M:%S.%f"
|
|
711
|
-
self.start_timestamp = webhook.start_time_millis
|
|
712
|
-
self.start_utc_time = TimeUtil.millis_to_format(self.start_timestamp, fmt, "UTC")
|
|
713
|
-
self.start_server_time = None
|
|
714
|
-
self.start_user_time = TimeUtil.millis_to_format(self.start_timestamp, fmt, webhook.user_utc_offset_seconds)
|
|
715
|
-
|
|
716
|
-
self.timestamp = TimeUtil.now_in_millis()
|
|
717
|
-
self.utc_time = TimeUtil.millis_to_format(self.timestamp, fmt, "UTC")
|
|
718
|
-
self.server_time = None
|
|
719
|
-
self.user_time = TimeUtil.millis_to_format(self.timestamp, fmt, webhook.user_utc_offset_seconds)
|
|
720
|
-
|
|
721
|
-
if TimeUtil.get_default_timezone():
|
|
722
|
-
self.start_server_time = TimeUtil.millis_to_format(self.start_timestamp, fmt)
|
|
723
|
-
self.server_time = TimeUtil.millis_to_format(self.timestamp, fmt)
|
|
724
|
-
|
|
725
|
-
# Webhook invocation information.
|
|
726
|
-
self.invocation_type = webhook.context.end_point_type.display_name
|
|
727
|
-
self.class_name = webhook.__class__.__name__
|
|
728
|
-
|
|
729
|
-
# User location information.
|
|
730
|
-
self.link = None
|
|
731
|
-
context = webhook.context
|
|
732
|
-
navigator = SapioNavigationLinker(context)
|
|
733
|
-
if context.eln_experiment is not None:
|
|
734
|
-
self.link = navigator.experiment(context.eln_experiment)
|
|
735
|
-
elif context.base_data_record:
|
|
736
|
-
self.link = navigator.data_record(context.base_data_record)
|
|
737
|
-
elif context.data_record and not context.data_record_list:
|
|
738
|
-
self.link = navigator.data_record(context.data_record)
|
|
739
|
-
|
|
740
|
-
# If this is logging an exception, get the system's info on the most recent exception and produce a summary.
|
|
741
|
-
self.exc_summary = None
|
|
742
|
-
if summarize_exception:
|
|
743
|
-
exc_type, exc_value, exc_traceback = sys.exc_info()
|
|
744
|
-
|
|
745
|
-
# If this is a SapioServerException, then it can contain json with the exception message from the server.
|
|
746
|
-
# This provides a more useful summary than args[0] does, so use it instead.
|
|
747
|
-
exception_name: str = exc_type.__name__
|
|
748
|
-
if exception_name == "SapioServerException":
|
|
749
|
-
# Sometimes the SapioServerException is HTML instead of JSON. If that's the case, just try/catch the
|
|
750
|
-
# failure to parse the JSON and continue to use args[0] as the exception message.
|
|
751
|
-
try:
|
|
752
|
-
exc_str: str = str(exc_value)
|
|
753
|
-
exception_msg = json.loads(exc_str[exc_str.find("{"):]).get("message")
|
|
754
|
-
except Exception:
|
|
755
|
-
exception_msg = exc_value.args[0]
|
|
756
|
-
else:
|
|
757
|
-
# For all other exceptions, assume that the first argument is a message.
|
|
758
|
-
exception_msg = exc_value.args[0]
|
|
759
|
-
|
|
760
|
-
self.exc_summary = f"{exception_name}: {exception_msg}"
|
|
761
|
-
del (exc_type, exc_value, exc_traceback)
|
|
762
|
-
|
|
763
|
-
def __str__(self) -> str:
|
|
764
|
-
message: str = ""
|
|
765
|
-
|
|
766
|
-
# Record the time that the webhook was started and this state was created.
|
|
767
|
-
message += "Webhook Invocation Time:\n"
|
|
768
|
-
message += f"\tUTC: {self.start_utc_time}\n"
|
|
769
|
-
if TimeUtil.get_default_timezone() is not None:
|
|
770
|
-
message += f"\tServer: {self.start_server_time}\n"
|
|
771
|
-
message += f"\tUser: {self.start_user_time}\n"
|
|
772
|
-
|
|
773
|
-
message += "Current Time:\n"
|
|
774
|
-
message += f"\tUTC: {self.utc_time}\n"
|
|
775
|
-
if TimeUtil.get_default_timezone() is not None:
|
|
776
|
-
message += f"\tServer: {self.server_time}\n"
|
|
777
|
-
message += f"\tUser: {self.user_time}\n"
|
|
778
|
-
|
|
779
|
-
# Record information about the user and how the webhook was invoked.
|
|
780
|
-
message += f"Username: {self.username}\n"
|
|
781
|
-
message += f"User group: {self.user_group}\n"
|
|
782
|
-
message += f"Webhook invocation type: {self.invocation_type}\n"
|
|
783
|
-
message += f"Class name: {self.class_name}\n"
|
|
784
|
-
|
|
785
|
-
# If we're able to, provide a link to the location that the error occurred at.
|
|
786
|
-
if self.link:
|
|
787
|
-
message += f"User location: {self.link}\n"
|
|
788
|
-
|
|
789
|
-
# If this state summary is for an exception, include the exception summary.
|
|
790
|
-
if self.exc_summary:
|
|
791
|
-
message += f"{self.exc_summary}\n"
|
|
792
|
-
|
|
793
|
-
return message
|
|
@@ -3,7 +3,7 @@ import traceback
|
|
|
3
3
|
from abc import abstractmethod, ABC
|
|
4
4
|
from base64 import b64decode
|
|
5
5
|
from logging import Logger
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
8
|
from flask import request, Response, Request
|
|
9
9
|
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
@@ -122,7 +122,7 @@ class AbstractWebserviceHandler(AbstractWebhookHandler):
|
|
|
122
122
|
"""
|
|
123
123
|
pass
|
|
124
124
|
|
|
125
|
-
def authenticate_user(self, headers:
|
|
125
|
+
def authenticate_user(self, headers: dict[str, str]) -> SapioUser:
|
|
126
126
|
"""
|
|
127
127
|
Authenticate a user for making requests to a Sapio server using the provided headers. If no user can be
|
|
128
128
|
authenticated, then an exception will be thrown.
|