sapiopycommons 2025.4.9a150__py3-none-any.whl → 2025.4.9a476__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 +1262 -392
- sapiopycommons/callbacks/field_builder.py +2 -0
- sapiopycommons/chem/Molecules.py +0 -2
- sapiopycommons/customreport/auto_pagers.py +281 -0
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/attachment_util.py +4 -2
- sapiopycommons/datatype/data_fields.py +23 -1
- sapiopycommons/eln/experiment_cache.py +173 -0
- sapiopycommons/eln/experiment_handler.py +933 -279
- sapiopycommons/eln/experiment_report_util.py +15 -10
- sapiopycommons/eln/experiment_step_factory.py +474 -0
- sapiopycommons/eln/experiment_tags.py +7 -0
- sapiopycommons/eln/plate_designer.py +159 -59
- sapiopycommons/eln/step_creation.py +235 -0
- sapiopycommons/files/file_bridge.py +76 -0
- sapiopycommons/files/file_bridge_handler.py +325 -110
- sapiopycommons/files/file_data_handler.py +2 -2
- sapiopycommons/files/file_util.py +40 -15
- sapiopycommons/files/file_validator.py +6 -5
- 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 +51 -28
- sapiopycommons/general/audit_log.py +2 -2
- sapiopycommons/general/custom_report_util.py +24 -1
- sapiopycommons/general/data_structure_util.py +115 -0
- sapiopycommons/general/directive_util.py +86 -0
- sapiopycommons/general/exceptions.py +41 -2
- sapiopycommons/general/popup_util.py +2 -2
- sapiopycommons/multimodal/multimodal.py +1 -0
- sapiopycommons/processtracking/custom_workflow_handler.py +46 -30
- sapiopycommons/recordmodel/record_handler.py +547 -159
- sapiopycommons/rules/eln_rule_handler.py +41 -30
- sapiopycommons/rules/on_save_rule_handler.py +41 -30
- sapiopycommons/samples/aliquot.py +48 -0
- sapiopycommons/webhook/webhook_handlers.py +448 -55
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.4.9a150.dist-info → sapiopycommons-2025.4.9a476.dist-info}/METADATA +1 -1
- sapiopycommons-2025.4.9a476.dist-info/RECORD +67 -0
- sapiopycommons-2025.4.9a150.dist-info/RECORD +0 -59
- {sapiopycommons-2025.4.9a150.dist-info → sapiopycommons-2025.4.9a476.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.4.9a150.dist-info → sapiopycommons-2025.4.9a476.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,28 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
1
6
|
import traceback
|
|
2
7
|
from abc import abstractmethod
|
|
3
8
|
from logging import Logger
|
|
4
9
|
|
|
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
|
|
5
14
|
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
6
15
|
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
|
|
7
22
|
from sapiopylib.rest.User import SapioUser
|
|
8
23
|
from sapiopylib.rest.WebhookService import AbstractWebhookHandler
|
|
9
24
|
from sapiopylib.rest.pojo.Message import VeloxLogMessage, VeloxLogLevel
|
|
25
|
+
from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import PopupType
|
|
10
26
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
11
27
|
from sapiopylib.rest.pojo.webhook.WebhookEnums import WebhookEndpointType
|
|
12
28
|
from sapiopylib.rest.pojo.webhook.WebhookResult import SapioWebhookResult
|
|
13
29
|
from sapiopylib.rest.utils.DataTypeCacheManager import DataTypeCacheManager
|
|
30
|
+
from sapiopylib.rest.utils.FoundationAccessioning import FoundationAccessionManager
|
|
14
31
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager, \
|
|
15
32
|
RecordModelRelationshipManager
|
|
16
33
|
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
34
|
+
from sapiopylib.rest.utils.recordmodel.last_saved import LastSavedValueManager
|
|
17
35
|
|
|
18
36
|
from sapiopycommons.callbacks.callback_util import CallbackUtil
|
|
19
37
|
from sapiopycommons.eln.experiment_handler import ExperimentHandler
|
|
38
|
+
from sapiopycommons.general.directive_util import DirectiveUtil
|
|
20
39
|
from sapiopycommons.general.exceptions import SapioUserErrorException, SapioCriticalErrorException, \
|
|
21
|
-
SapioUserCancelledException, SapioException, SapioDialogTimeoutException
|
|
40
|
+
SapioUserCancelledException, SapioException, SapioDialogTimeoutException, MessageDisplayType
|
|
22
41
|
from sapiopycommons.general.sapio_links import SapioNavigationLinker
|
|
42
|
+
from sapiopycommons.general.time_util import TimeUtil
|
|
23
43
|
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
24
44
|
from sapiopycommons.rules.eln_rule_handler import ElnRuleHandler
|
|
25
45
|
from sapiopycommons.rules.on_save_rule_handler import OnSaveRuleHandler
|
|
46
|
+
from sapiopycommons.webhook.webhook_context import ProcessQueueContext
|
|
26
47
|
|
|
27
48
|
|
|
28
49
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
@@ -34,37 +55,156 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
34
55
|
"""
|
|
35
56
|
logger: Logger
|
|
36
57
|
|
|
58
|
+
_start_time: float
|
|
59
|
+
_start_time_epoch: int
|
|
60
|
+
|
|
37
61
|
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."""
|
|
38
68
|
context: SapioWebhookContext
|
|
39
|
-
|
|
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."""
|
|
40
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
|
+
|
|
41
101
|
rec_man: RecordModelManager
|
|
102
|
+
"""The record model manager. Used for committing record model changes to the system."""
|
|
42
103
|
inst_man: RecordModelInstanceManager
|
|
104
|
+
"""The record model instance manager. Used for adding record models to the cache."""
|
|
43
105
|
rel_man: RecordModelRelationshipManager
|
|
106
|
+
"""The record model relationship manager. Used for loading parent/child and side-link relationships between record
|
|
107
|
+
models."""
|
|
44
108
|
# FR-46329: Add the ancestor manager to CommonsWebhookHandler.
|
|
45
109
|
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."""
|
|
46
113
|
|
|
47
114
|
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. """
|
|
48
117
|
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."""
|
|
49
121
|
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."""
|
|
50
125
|
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. """
|
|
51
128
|
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."""
|
|
52
163
|
|
|
53
164
|
def run(self, context: SapioWebhookContext) -> SapioWebhookResult:
|
|
54
|
-
|
|
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.
|
|
55
170
|
self.context = context
|
|
56
171
|
|
|
172
|
+
# Save the user and commonly sought after user information.
|
|
173
|
+
self.user = context.user
|
|
174
|
+
self.group_name = self.user.session_additional_data.current_group_name
|
|
175
|
+
self.user_utc_offset_seconds = self.user.session_additional_data.utc_offset_seconds
|
|
176
|
+
|
|
177
|
+
# Get the logger from the user.
|
|
57
178
|
self.logger = self.user.logger
|
|
58
179
|
|
|
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)
|
|
59
185
|
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.
|
|
60
196
|
self.rec_man = RecordModelManager(self.user)
|
|
61
197
|
self.inst_man = self.rec_man.instance_manager
|
|
62
198
|
self.rel_man = self.rec_man.relationship_manager
|
|
63
|
-
self.an_man =
|
|
199
|
+
self.an_man = self.rec_man.ancestor_manager
|
|
200
|
+
self.saved_vals_man = self.rec_man.last_saved_manager
|
|
64
201
|
|
|
202
|
+
# Initialize more complex classes from sapiopylib and sapiopycommons.
|
|
203
|
+
self.fnd_acc_man = FoundationAccessionManager(self.user)
|
|
65
204
|
self.dt_cache = DataTypeCacheManager(self.user)
|
|
66
205
|
self.rec_handler = RecordHandler(context)
|
|
67
206
|
self.callback = CallbackUtil(context)
|
|
207
|
+
self.directive = DirectiveUtil(context)
|
|
68
208
|
if context.eln_experiment is not None:
|
|
69
209
|
self.exp_handler = ExperimentHandler(context)
|
|
70
210
|
else:
|
|
@@ -75,6 +215,29 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
75
215
|
self.rule_handler = ElnRuleHandler(context)
|
|
76
216
|
else:
|
|
77
217
|
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.")
|
|
78
241
|
|
|
79
242
|
# Wrap the execution of each webhook in a try/catch. If an exception occurs, handle any special sapiopycommons
|
|
80
243
|
# exceptions. Otherwise, return a generic message stating that an error occurred.
|
|
@@ -124,7 +287,18 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
124
287
|
if result is not None:
|
|
125
288
|
return result
|
|
126
289
|
self.log_error(traceback.format_exc())
|
|
127
|
-
|
|
290
|
+
self.handle_user_error_exception_extra(e)
|
|
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
|
|
128
302
|
|
|
129
303
|
def handle_critical_error_exception(self, e: SapioCriticalErrorException) -> SapioWebhookResult:
|
|
130
304
|
"""
|
|
@@ -139,13 +313,18 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
139
313
|
if result is not None:
|
|
140
314
|
return result
|
|
141
315
|
self.log_error(traceback.format_exc())
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if self.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
316
|
+
self.handle_critical_error_exception_extra(e)
|
|
317
|
+
|
|
318
|
+
display_type: MessageDisplayType = e.display_type if e.display_type else self.default_critical_error_display_type
|
|
319
|
+
title: str = e.title if e.title is not None else self.default_critical_error_title
|
|
320
|
+
return self._display_exception(e.msg, display_type, title)
|
|
321
|
+
|
|
322
|
+
def handle_critical_error_exception_extra(self, e: SapioCriticalErrorException) -> None:
|
|
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
|
|
149
328
|
|
|
150
329
|
def handle_user_cancelled_exception(self, e: SapioUserCancelledException) -> SapioWebhookResult:
|
|
151
330
|
"""
|
|
@@ -160,7 +339,17 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
160
339
|
result: SapioWebhookResult | None = self.handle_any_exception(e)
|
|
161
340
|
if result is not None:
|
|
162
341
|
return result
|
|
163
|
-
|
|
342
|
+
self.handle_user_cancelled_exception_extra(e)
|
|
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
|
|
164
353
|
|
|
165
354
|
def handle_dialog_timeout_exception(self, e: SapioDialogTimeoutException) -> SapioWebhookResult:
|
|
166
355
|
"""
|
|
@@ -175,15 +364,17 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
175
364
|
result: SapioWebhookResult | None = self.handle_any_exception(e)
|
|
176
365
|
if result is not None:
|
|
177
366
|
return result
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
367
|
+
self.handle_dialog_timeout_exception_extra(e)
|
|
368
|
+
return self._display_exception(self.default_dialog_timeout_message,
|
|
369
|
+
self.default_dialog_timeout_display_type,
|
|
370
|
+
self.default_dialog_timeout_title)
|
|
371
|
+
|
|
372
|
+
def handle_dialog_timeout_exception_extra(self, e: SapioDialogTimeoutException) -> None:
|
|
373
|
+
"""
|
|
374
|
+
An additional function that can be overridden to provide extra behavior when a SapioDialogTimeoutException is
|
|
375
|
+
thrown. Default behavior does nothing.
|
|
376
|
+
"""
|
|
377
|
+
pass
|
|
187
378
|
|
|
188
379
|
def handle_unexpected_exception(self, e: Exception) -> SapioWebhookResult:
|
|
189
380
|
"""
|
|
@@ -200,13 +391,21 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
200
391
|
if result is not None:
|
|
201
392
|
return result
|
|
202
393
|
msg: str = traceback.format_exc()
|
|
203
|
-
self.log_error(msg)
|
|
394
|
+
self.log_error(msg, True)
|
|
204
395
|
# FR-47079: Also log all unexpected exception messages to the webhook execution log within the platform.
|
|
205
|
-
self.log_error_to_webhook_execution_log(msg)
|
|
206
|
-
|
|
207
|
-
|
|
396
|
+
self.log_error_to_webhook_execution_log(msg, True)
|
|
397
|
+
self.handle_unexpected_exception_extra(e)
|
|
398
|
+
return self._display_exception(self.default_unexpected_error_message,
|
|
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
|
|
208
408
|
|
|
209
|
-
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
210
409
|
def handle_any_exception(self, e: Exception) -> SapioWebhookResult | None:
|
|
211
410
|
"""
|
|
212
411
|
An exception handler which runs regardless of the type of exception that was raised. Can be used to "rollback"
|
|
@@ -217,7 +416,55 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
217
416
|
sent by one of the normal exception handlers, or may return None if no result needs returned. If a result is
|
|
218
417
|
returned, then the default behavior of other exception handlers is skipped.
|
|
219
418
|
"""
|
|
220
|
-
|
|
419
|
+
pass
|
|
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)
|
|
221
468
|
|
|
222
469
|
def log_info(self, msg: str) -> None:
|
|
223
470
|
"""
|
|
@@ -227,55 +474,69 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
227
474
|
"""
|
|
228
475
|
self.logger.info(self._format_log(msg, "log_info call"))
|
|
229
476
|
|
|
230
|
-
def log_error(self, msg: str) -> None:
|
|
477
|
+
def log_error(self, msg: str, is_exception: bool = False) -> None:
|
|
231
478
|
"""
|
|
232
479
|
Write an info message to the webhook server log. Log destination is stdout. This message will include
|
|
233
480
|
information about the user's group, their location in the system, the webhook invocation type, and other
|
|
234
481
|
important information that can be gathered from the context that is useful for debugging.
|
|
235
482
|
"""
|
|
236
483
|
# PR-46209: Use logger.error instead of logger.info when logging errors.
|
|
237
|
-
self.logger.error(self._format_log(msg, "log_error call"))
|
|
484
|
+
self.logger.error(self._format_log(msg, "log_error call", is_exception))
|
|
238
485
|
|
|
239
|
-
def log_error_to_webhook_execution_log(self, msg: str) -> None:
|
|
486
|
+
def log_error_to_webhook_execution_log(self, msg: str, is_exception: bool = False) -> None:
|
|
240
487
|
"""
|
|
241
488
|
Write an error message to the platform's webhook execution log. This can be reviewed by navigating to the
|
|
242
489
|
webhook configuration where the webhook that called this function is defined and clicking the "View Log"
|
|
243
490
|
button. From there, select one of the rows for the webhook executions and click "Download Log" from the right
|
|
244
491
|
side table.
|
|
245
492
|
"""
|
|
246
|
-
|
|
247
|
-
messenger.log_message(VeloxLogMessage(
|
|
248
|
-
log_level=VeloxLogLevel.ERROR,
|
|
249
|
-
originating_class=self.__class__.__name__))
|
|
493
|
+
msg = self._format_log(msg, "Error occurred during webhook execution.", is_exception)
|
|
494
|
+
self.messenger.log_message(VeloxLogMessage(msg, VeloxLogLevel.ERROR, self.__class__.__name__))
|
|
250
495
|
|
|
251
|
-
def _format_log(self, msg: str, prefix: str | None = None) -> str:
|
|
496
|
+
def _format_log(self, msg: str, prefix: str | None = None, is_exception: bool = False) -> str:
|
|
252
497
|
"""
|
|
253
498
|
Given a message to log, populate it with some metadata about this particular webhook execution, including
|
|
254
499
|
the group of the user and the invocation type of the webhook call.
|
|
255
500
|
"""
|
|
256
|
-
#
|
|
257
|
-
|
|
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
|
|
501
|
+
# Start the message with the provided prefix.
|
|
502
|
+
message: str = prefix + "\n" if prefix else ""
|
|
266
503
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
message
|
|
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"
|
|
504
|
+
# Construct a summary of the current state of this webhook.
|
|
505
|
+
message += f"{WebhookStateSummary(self, is_exception)}\n"
|
|
506
|
+
|
|
507
|
+
# End the message with the provided msg parameter.
|
|
276
508
|
message += msg
|
|
277
509
|
return message
|
|
278
510
|
|
|
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
|
+
|
|
279
540
|
def is_main_toolbar(self) -> bool:
|
|
280
541
|
"""
|
|
281
542
|
:return: True if this endpoint was invoked as a main toolbar button.
|
|
@@ -398,3 +659,135 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
398
659
|
:return: Whether client callbacks and directives can be sent from this webhook's endpoint type.
|
|
399
660
|
"""
|
|
400
661
|
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, Mapping
|
|
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: Mapping[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.
|