sapiopycommons 2024.8.28a313__py3-none-any.whl → 2024.8.28a315__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 +407 -69
- sapiopycommons/chem/IndigoMolecules.py +1 -0
- sapiopycommons/chem/Molecules.py +1 -0
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +60 -0
- sapiopycommons/customreport/custom_report_builder.py +125 -0
- sapiopycommons/customreport/term_builder.py +299 -0
- sapiopycommons/datatype/attachment_util.py +11 -10
- sapiopycommons/eln/experiment_handler.py +209 -48
- sapiopycommons/eln/experiment_report_util.py +118 -0
- sapiopycommons/files/complex_data_loader.py +5 -4
- sapiopycommons/files/file_bridge.py +31 -24
- sapiopycommons/files/file_bridge_handler.py +340 -0
- sapiopycommons/files/file_data_handler.py +2 -5
- sapiopycommons/files/file_util.py +50 -10
- sapiopycommons/files/file_validator.py +92 -6
- sapiopycommons/files/file_writer.py +44 -15
- sapiopycommons/general/accession_service.py +375 -0
- sapiopycommons/general/aliases.py +147 -3
- sapiopycommons/general/audit_log.py +196 -0
- sapiopycommons/general/custom_report_util.py +211 -37
- sapiopycommons/general/popup_util.py +17 -0
- sapiopycommons/general/sapio_links.py +50 -0
- sapiopycommons/general/time_util.py +40 -0
- sapiopycommons/multimodal/multimodal.py +146 -0
- sapiopycommons/multimodal/multimodal_data.py +486 -0
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +481 -97
- sapiopycommons/rules/eln_rule_handler.py +34 -25
- sapiopycommons/rules/on_save_rule_handler.py +34 -31
- sapiopycommons/webhook/webhook_handlers.py +147 -26
- sapiopycommons/webhook/webservice_handlers.py +67 -0
- {sapiopycommons-2024.8.28a313.dist-info → sapiopycommons-2024.8.28a315.dist-info}/METADATA +4 -2
- sapiopycommons-2024.8.28a315.dist-info/RECORD +50 -0
- sapiopycommons-2024.8.28a313.dist-info/RECORD +0 -38
- {sapiopycommons-2024.8.28a313.dist-info → sapiopycommons-2024.8.28a315.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.8.28a313.dist-info → sapiopycommons-2024.8.28a315.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from weakref import WeakValueDictionary
|
|
4
|
+
|
|
5
|
+
from sapiopylib.rest.User import SapioUser
|
|
1
6
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
2
|
-
from sapiopylib.rest.pojo.
|
|
7
|
+
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
3
8
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
4
9
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager
|
|
5
10
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
6
11
|
|
|
7
|
-
from sapiopycommons.general.aliases import FieldMap
|
|
12
|
+
from sapiopycommons.general.aliases import FieldMap, AliasUtil, DataTypeIdentifier
|
|
8
13
|
from sapiopycommons.general.exceptions import SapioException
|
|
9
14
|
|
|
10
15
|
|
|
@@ -12,7 +17,6 @@ from sapiopycommons.general.exceptions import SapioException
|
|
|
12
17
|
class ElnRuleHandler:
|
|
13
18
|
"""
|
|
14
19
|
A class which helps with the parsing and navigation of the ELN rule result map of a webhook context.
|
|
15
|
-
TODO: Add functionality around the VeloxRuleType of the rule results.
|
|
16
20
|
"""
|
|
17
21
|
__context: SapioWebhookContext
|
|
18
22
|
"""The context that this handler is working from."""
|
|
@@ -34,7 +38,25 @@ class ElnRuleHandler:
|
|
|
34
38
|
"""A mapping of entry name to the lists of field maps for that entry, each grouping of field maps being mapped by
|
|
35
39
|
its data type."""
|
|
36
40
|
|
|
41
|
+
__instances: WeakValueDictionary[SapioUser, ElnRuleHandler] = WeakValueDictionary()
|
|
42
|
+
__initialized: bool
|
|
43
|
+
|
|
44
|
+
def __new__(cls, context: SapioWebhookContext):
|
|
45
|
+
if context.velox_eln_rule_result_map is None:
|
|
46
|
+
raise SapioException("No Velox ELN rule result map in context for ElnRuleHandler to parse.")
|
|
47
|
+
user = context if isinstance(context, SapioUser) else context.user
|
|
48
|
+
obj = cls.__instances.get(user)
|
|
49
|
+
if not obj:
|
|
50
|
+
obj = object.__new__(cls)
|
|
51
|
+
obj.__initialized = False
|
|
52
|
+
cls.__instances[user] = obj
|
|
53
|
+
return obj
|
|
54
|
+
|
|
37
55
|
def __init__(self, context: SapioWebhookContext):
|
|
56
|
+
if self.__initialized:
|
|
57
|
+
return
|
|
58
|
+
self.__initialized = True
|
|
59
|
+
|
|
38
60
|
if context.velox_eln_rule_result_map is None:
|
|
39
61
|
raise SapioException("No Velox ELN rule result map in context for ElnRuleHandler to parse.")
|
|
40
62
|
self.__context = context
|
|
@@ -64,13 +86,8 @@ class ElnRuleHandler:
|
|
|
64
86
|
# Get the data type of this record. If this is an ELN type, ignore the digits.
|
|
65
87
|
data_type: str = record.data_type_name
|
|
66
88
|
# PR-46331: Ensure that all ELN types are converted to their base data type name.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
data_type = "ELNExperiment"
|
|
70
|
-
elif data_type.startswith("ELNExperimentDetail_"):
|
|
71
|
-
data_type = "ELNExperimentDetail"
|
|
72
|
-
elif data_type.startswith("ELNSampleDetail_"):
|
|
73
|
-
data_type = "ELNSampleDetail"
|
|
89
|
+
if ElnBaseDataType.is_eln_type(data_type):
|
|
90
|
+
data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
|
|
74
91
|
# Update the list of records of this type that exist so far globally.
|
|
75
92
|
self.__records.setdefault(data_type, set()).add(record)
|
|
76
93
|
# Do the same for the list of records of this type for this specific entry.
|
|
@@ -85,19 +102,9 @@ class ElnRuleHandler:
|
|
|
85
102
|
entry_dict: dict[str, dict[int, FieldMap]] = {}
|
|
86
103
|
for record_result in entry_results:
|
|
87
104
|
for result in record_result.velox_type_rule_field_map_result_list:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if isinstance(velox_type, dict):
|
|
92
|
-
velox_type: VeloxRuleType = VeloxRuleParser.parse_velox_rule_type(velox_type)
|
|
93
|
-
data_type: str = velox_type.data_type_name
|
|
94
|
-
# TODO: Use ElnBaseDataType.is_eln_type when it is no longer bugged in sapiopylib.
|
|
95
|
-
if data_type.startswith("ELNExperiment_"):
|
|
96
|
-
data_type = "ELNExperiment"
|
|
97
|
-
elif data_type.startswith("ELNExperimentDetail_"):
|
|
98
|
-
data_type = "ELNExperimentDetail"
|
|
99
|
-
elif data_type.startswith("ELNSampleDetail_"):
|
|
100
|
-
data_type = "ELNSampleDetail"
|
|
105
|
+
data_type: str = result.velox_type_pojo.data_type_name
|
|
106
|
+
if ElnBaseDataType.is_eln_type(data_type):
|
|
107
|
+
data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
|
|
101
108
|
for field_map in result.field_map_list:
|
|
102
109
|
rec_id: int = field_map.get("RecordId")
|
|
103
110
|
self.__field_maps.setdefault(data_type, {}).update({rec_id: field_map})
|
|
@@ -118,7 +125,7 @@ class ElnRuleHandler:
|
|
|
118
125
|
"""
|
|
119
126
|
return list(self.__entry_to_field_maps.keys())
|
|
120
127
|
|
|
121
|
-
def get_records(self, data_type:
|
|
128
|
+
def get_records(self, data_type: DataTypeIdentifier, entry: str | None = None) -> list[DataRecord]:
|
|
122
129
|
"""
|
|
123
130
|
Get records from the cached context with the given data type. Capable of being filtered to searching within
|
|
124
131
|
the context of an entry name. If the given data type or entry does not exist in the context,
|
|
@@ -129,11 +136,12 @@ class ElnRuleHandler:
|
|
|
129
136
|
type from every entry. If an entry is provided, but it does not exist in the context, returns an empty list.
|
|
130
137
|
:return: The records from the context that match the input parameters.
|
|
131
138
|
"""
|
|
139
|
+
data_type: str = AliasUtil.to_data_type_name(data_type)
|
|
132
140
|
records: dict[str, set[DataRecord]] = self.__entry_to_records.get(entry, {}) if entry else self.__records
|
|
133
141
|
return list(records.get(data_type, []))
|
|
134
142
|
|
|
135
143
|
# FR-46701: Add functions to the rule handlers for accessing the field maps of inaccessible records in the context.
|
|
136
|
-
def get_field_maps(self, data_type:
|
|
144
|
+
def get_field_maps(self, data_type: DataTypeIdentifier, entry: str | None = None) -> list[FieldMap]:
|
|
137
145
|
"""
|
|
138
146
|
Get field maps from the cached context with the given data type. Capable of being filtered to searching within
|
|
139
147
|
the context of an entry name. If the given data type or entry does not exist in the context,
|
|
@@ -149,6 +157,7 @@ class ElnRuleHandler:
|
|
|
149
157
|
list.
|
|
150
158
|
:return: The field maps from the context that match the input parameters.
|
|
151
159
|
"""
|
|
160
|
+
data_type: str = AliasUtil.to_data_type_name(data_type)
|
|
152
161
|
field_maps: dict[str, dict[int, FieldMap]] = self.__entry_to_field_maps.get(entry, {}) if entry else self.__field_maps
|
|
153
162
|
return list(field_maps.get(data_type, {}).values())
|
|
154
163
|
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from weakref import WeakValueDictionary
|
|
4
|
+
|
|
5
|
+
from sapiopylib.rest.User import SapioUser
|
|
1
6
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
2
|
-
from sapiopylib.rest.pojo.
|
|
7
|
+
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
3
8
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
4
9
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager
|
|
5
10
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
6
11
|
|
|
7
|
-
from sapiopycommons.general.aliases import FieldMap
|
|
12
|
+
from sapiopycommons.general.aliases import FieldMap, DataTypeIdentifier, AliasUtil
|
|
8
13
|
from sapiopycommons.general.exceptions import SapioException
|
|
9
14
|
|
|
10
15
|
|
|
@@ -12,7 +17,6 @@ from sapiopycommons.general.exceptions import SapioException
|
|
|
12
17
|
class OnSaveRuleHandler:
|
|
13
18
|
"""
|
|
14
19
|
A class which helps with the parsing and navigation of the on save rule result map of a webhook context.
|
|
15
|
-
TODO: Add functionality around the VeloxRuleType of the rule results.
|
|
16
20
|
"""
|
|
17
21
|
__context: SapioWebhookContext
|
|
18
22
|
"""The context that this handler is working from."""
|
|
@@ -34,7 +38,25 @@ class OnSaveRuleHandler:
|
|
|
34
38
|
"""A mapping of record IDs of records in the context.data_record_list to the field maps related to that
|
|
35
39
|
record, each grouping of field maps being mapped by its data type."""
|
|
36
40
|
|
|
41
|
+
__instances: WeakValueDictionary[SapioUser, OnSaveRuleHandler] = WeakValueDictionary()
|
|
42
|
+
__initialized: bool
|
|
43
|
+
|
|
44
|
+
def __new__(cls, context: SapioWebhookContext):
|
|
45
|
+
if context.velox_on_save_result_map is None:
|
|
46
|
+
raise SapioException("No Velox on save rule result map in context for OnSaveRuleHandler to parse.")
|
|
47
|
+
user = context if isinstance(context, SapioUser) else context.user
|
|
48
|
+
obj = cls.__instances.get(user)
|
|
49
|
+
if not obj:
|
|
50
|
+
obj = object.__new__(cls)
|
|
51
|
+
obj.__initialized = False
|
|
52
|
+
cls.__instances[user] = obj
|
|
53
|
+
return obj
|
|
54
|
+
|
|
37
55
|
def __init__(self, context: SapioWebhookContext):
|
|
56
|
+
if self.__initialized:
|
|
57
|
+
return
|
|
58
|
+
self.__initialized = True
|
|
59
|
+
|
|
38
60
|
if context.velox_on_save_result_map is None:
|
|
39
61
|
raise SapioException("No Velox on save rule result map in context for OnSaveRuleHandler to parse.")
|
|
40
62
|
self.__context = context
|
|
@@ -51,9 +73,6 @@ class OnSaveRuleHandler:
|
|
|
51
73
|
self.__base_id_to_records = {}
|
|
52
74
|
# Each record ID in the context has a list of results for that record.
|
|
53
75
|
for record_id, rule_results in self.__context.velox_on_save_result_map.items():
|
|
54
|
-
# TODO: Record IDs are currently being stored in the map as strings instead of ints. This can be removed
|
|
55
|
-
# once sapiopylib is fixed.
|
|
56
|
-
record_id = int(record_id)
|
|
57
76
|
# Keep track of the records for this specific record ID.
|
|
58
77
|
id_dict: dict[str, set[DataRecord]] = {}
|
|
59
78
|
# The list of results for a record consist of a list of data records and a VeloxType that specifies
|
|
@@ -64,13 +83,8 @@ class OnSaveRuleHandler:
|
|
|
64
83
|
# Get the data type of this record. If this is an ELN type, ignore the digits.
|
|
65
84
|
data_type: str = record.data_type_name
|
|
66
85
|
# PR-46331: Ensure that all ELN types are converted to their base data type name.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
data_type = "ELNExperiment"
|
|
70
|
-
elif data_type.startswith("ELNExperimentDetail_"):
|
|
71
|
-
data_type = "ELNExperimentDetail"
|
|
72
|
-
elif data_type.startswith("ELNSampleDetail_"):
|
|
73
|
-
data_type = "ELNSampleDetail"
|
|
86
|
+
if ElnBaseDataType.is_eln_type(data_type):
|
|
87
|
+
data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
|
|
74
88
|
# Update the list of records of this type that exist so far globally.
|
|
75
89
|
self.__records.setdefault(data_type, set()).add(record)
|
|
76
90
|
# Do the same for the list of records of this type that relate to this record ID.
|
|
@@ -82,24 +96,11 @@ class OnSaveRuleHandler:
|
|
|
82
96
|
self.__base_id_to_field_maps = {}
|
|
83
97
|
# Repeat the same thing for the field map results.
|
|
84
98
|
for record_id, rule_results in self.__context.velox_on_save_field_map_result_map.items():
|
|
85
|
-
# TODO: Record IDs are currently being stored in the map as strings instead of ints. This can be removed
|
|
86
|
-
# once sapiopylib is fixed.
|
|
87
|
-
record_id = int(record_id)
|
|
88
99
|
id_dict: dict[str, dict[int, FieldMap]] = {}
|
|
89
100
|
for record_result in rule_results:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if isinstance(velox_type, dict):
|
|
94
|
-
velox_type: VeloxRuleType = VeloxRuleParser.parse_velox_rule_type(velox_type)
|
|
95
|
-
data_type: str = velox_type.data_type_name
|
|
96
|
-
# TODO: Use ElnBaseDataType.is_eln_type when it is no longer bugged in sapiopylib.
|
|
97
|
-
if data_type.startswith("ELNExperiment_"):
|
|
98
|
-
data_type = "ELNExperiment"
|
|
99
|
-
elif data_type.startswith("ELNExperimentDetail_"):
|
|
100
|
-
data_type = "ELNExperimentDetail"
|
|
101
|
-
elif data_type.startswith("ELNSampleDetail_"):
|
|
102
|
-
data_type = "ELNSampleDetail"
|
|
101
|
+
data_type: str = record_result.velox_type_pojo.data_type_name
|
|
102
|
+
if ElnBaseDataType.is_eln_type(data_type):
|
|
103
|
+
data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
|
|
103
104
|
for field_map in record_result.field_map_list:
|
|
104
105
|
rec_id: int = field_map.get("RecordId")
|
|
105
106
|
self.__field_maps.setdefault(data_type, {}).update({rec_id: field_map})
|
|
@@ -120,7 +121,7 @@ class OnSaveRuleHandler:
|
|
|
120
121
|
"""
|
|
121
122
|
return list(self.__base_id_to_field_maps.keys())
|
|
122
123
|
|
|
123
|
-
def get_records(self, data_type:
|
|
124
|
+
def get_records(self, data_type: DataTypeIdentifier, record_id: int | None = None) -> list[DataRecord]:
|
|
124
125
|
"""
|
|
125
126
|
Get records from the cached context with the given data type. Capable of being filtered to searching within
|
|
126
127
|
the context of a record ID. If the given data type or record ID does not exist in the context,
|
|
@@ -131,11 +132,12 @@ class OnSaveRuleHandler:
|
|
|
131
132
|
data type from every ID. If an ID is provided, but it does not exist in the context, returns an empty list.
|
|
132
133
|
:return: The records from the context that match the input parameters.
|
|
133
134
|
"""
|
|
135
|
+
data_type: str = AliasUtil.to_data_type_name(data_type)
|
|
134
136
|
records: dict[str, set[DataRecord]] = self.__base_id_to_records.get(record_id, {}) if record_id else self.__records
|
|
135
137
|
return list(records.get(data_type, []))
|
|
136
138
|
|
|
137
139
|
# FR-46701: Add functions to the rule handlers for accessing the field maps of inaccessible records in the context.
|
|
138
|
-
def get_field_maps(self, data_type:
|
|
140
|
+
def get_field_maps(self, data_type: DataTypeIdentifier, record_id: int | None = None) -> list[FieldMap]:
|
|
139
141
|
"""
|
|
140
142
|
Get field maps from the cached context with the given data type. Capable of being filtered to searching within
|
|
141
143
|
the context of a record ID. If the given data type or record ID does not exist in the context,
|
|
@@ -151,6 +153,7 @@ class OnSaveRuleHandler:
|
|
|
151
153
|
list.
|
|
152
154
|
:return: The field maps from the context that match the input parameters.
|
|
153
155
|
"""
|
|
156
|
+
data_type: str = AliasUtil.to_data_type_name(data_type)
|
|
154
157
|
field_maps: dict[str, dict[int, FieldMap]] = self.__base_id_to_field_maps.get(record_id, {}) if record_id else self.__field_maps
|
|
155
158
|
return list(field_maps.get(data_type, {}).values())
|
|
156
159
|
|
|
@@ -4,16 +4,25 @@ from logging import Logger
|
|
|
4
4
|
|
|
5
5
|
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
6
6
|
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
7
|
+
from sapiopylib.rest.User import SapioUser
|
|
7
8
|
from sapiopylib.rest.WebhookService import AbstractWebhookHandler
|
|
9
|
+
from sapiopylib.rest.pojo.Message import VeloxLogMessage, VeloxLogLevel
|
|
8
10
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
9
11
|
from sapiopylib.rest.pojo.webhook.WebhookEnums import WebhookEndpointType
|
|
10
12
|
from sapiopylib.rest.pojo.webhook.WebhookResult import SapioWebhookResult
|
|
13
|
+
from sapiopylib.rest.utils.DataTypeCacheManager import DataTypeCacheManager
|
|
11
14
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager, \
|
|
12
15
|
RecordModelRelationshipManager
|
|
13
16
|
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
14
17
|
|
|
18
|
+
from sapiopycommons.callbacks.callback_util import CallbackUtil
|
|
19
|
+
from sapiopycommons.eln.experiment_handler import ExperimentHandler
|
|
15
20
|
from sapiopycommons.general.exceptions import SapioUserErrorException, SapioCriticalErrorException, \
|
|
16
|
-
SapioUserCancelledException
|
|
21
|
+
SapioUserCancelledException, SapioException
|
|
22
|
+
from sapiopycommons.general.sapio_links import SapioNavigationLinker
|
|
23
|
+
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
24
|
+
from sapiopycommons.rules.eln_rule_handler import ElnRuleHandler
|
|
25
|
+
from sapiopycommons.rules.on_save_rule_handler import OnSaveRuleHandler
|
|
17
26
|
|
|
18
27
|
|
|
19
28
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
@@ -25,6 +34,7 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
25
34
|
"""
|
|
26
35
|
logger: Logger
|
|
27
36
|
|
|
37
|
+
user: SapioUser
|
|
28
38
|
context: SapioWebhookContext
|
|
29
39
|
|
|
30
40
|
dr_man: DataRecordManager
|
|
@@ -34,20 +44,46 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
34
44
|
# FR-46329: Add the ancestor manager to CommonsWebhookHandler.
|
|
35
45
|
an_man: RecordModelAncestorManager
|
|
36
46
|
|
|
47
|
+
dt_cache: DataTypeCacheManager
|
|
48
|
+
rec_handler: RecordHandler
|
|
49
|
+
callback: CallbackUtil
|
|
50
|
+
exp_handler: ExperimentHandler | None
|
|
51
|
+
rule_handler: OnSaveRuleHandler | ElnRuleHandler | None
|
|
52
|
+
|
|
37
53
|
def run(self, context: SapioWebhookContext) -> SapioWebhookResult:
|
|
54
|
+
self.user = context.user
|
|
38
55
|
self.context = context
|
|
39
|
-
|
|
56
|
+
|
|
57
|
+
self.logger = self.user.logger
|
|
40
58
|
|
|
41
59
|
self.dr_man = context.data_record_manager
|
|
42
|
-
self.rec_man = RecordModelManager(
|
|
60
|
+
self.rec_man = RecordModelManager(self.user)
|
|
43
61
|
self.inst_man = self.rec_man.instance_manager
|
|
44
62
|
self.rel_man = self.rec_man.relationship_manager
|
|
45
63
|
self.an_man = RecordModelAncestorManager(self.rec_man)
|
|
46
64
|
|
|
65
|
+
self.dt_cache = DataTypeCacheManager(self.user)
|
|
66
|
+
self.rec_handler = RecordHandler(context)
|
|
67
|
+
self.callback = CallbackUtil(context)
|
|
68
|
+
if context.eln_experiment is not None:
|
|
69
|
+
self.exp_handler = ExperimentHandler(context)
|
|
70
|
+
else:
|
|
71
|
+
self.exp_handler = None
|
|
72
|
+
if self.is_on_save_rule():
|
|
73
|
+
self.rule_handler = OnSaveRuleHandler(context)
|
|
74
|
+
elif self.is_eln_rule():
|
|
75
|
+
self.rule_handler = ElnRuleHandler(context)
|
|
76
|
+
else:
|
|
77
|
+
self.rule_handler = None
|
|
78
|
+
|
|
47
79
|
# Wrap the execution of each webhook in a try/catch. If an exception occurs, handle any special sapiopycommons
|
|
48
80
|
# exceptions. Otherwise, return a generic message stating that an error occurred.
|
|
49
81
|
try:
|
|
50
|
-
|
|
82
|
+
self.initialize(context)
|
|
83
|
+
result = self.execute(context)
|
|
84
|
+
if result is None:
|
|
85
|
+
raise SapioException("Your execute function returned a None result! Don't forget your return statement!")
|
|
86
|
+
return result
|
|
51
87
|
except SapioUserErrorException as e:
|
|
52
88
|
return self.handle_user_error_exception(e)
|
|
53
89
|
except SapioCriticalErrorException as e:
|
|
@@ -57,6 +93,13 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
57
93
|
except Exception as e:
|
|
58
94
|
return self.handle_unexpected_exception(e)
|
|
59
95
|
|
|
96
|
+
def initialize(self, context: SapioWebhookContext) -> None:
|
|
97
|
+
"""
|
|
98
|
+
A function that can be optionally overridden by your webhooks to initialize additional instance variables,
|
|
99
|
+
or set up whatever else you wish to set up before the execute function is ran. Default behavior does nothing.
|
|
100
|
+
"""
|
|
101
|
+
pass
|
|
102
|
+
|
|
60
103
|
@abstractmethod
|
|
61
104
|
def execute(self, context: SapioWebhookContext) -> SapioWebhookResult:
|
|
62
105
|
"""
|
|
@@ -91,7 +134,8 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
91
134
|
if result is not None:
|
|
92
135
|
return result
|
|
93
136
|
self.log_error(traceback.format_exc())
|
|
94
|
-
|
|
137
|
+
if self.can_send_client_callback():
|
|
138
|
+
DataMgmtServer.get_client_callback(self.user).display_error(e.args[0])
|
|
95
139
|
return SapioWebhookResult(False)
|
|
96
140
|
|
|
97
141
|
def handle_unexpected_exception(self, e: Exception) -> SapioWebhookResult:
|
|
@@ -105,7 +149,10 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
105
149
|
result: SapioWebhookResult | None = self.handle_any_exception(e)
|
|
106
150
|
if result is not None:
|
|
107
151
|
return result
|
|
108
|
-
|
|
152
|
+
msg: str = traceback.format_exc()
|
|
153
|
+
self.log_error(msg)
|
|
154
|
+
# FR-47079: Also log all unexpected exception messages to the webhook execution log within the platform.
|
|
155
|
+
self.log_error_to_webhook_execution_log(msg)
|
|
109
156
|
return SapioWebhookResult(False, display_text="Unexpected error occurred during webhook execution. "
|
|
110
157
|
"Please contact Sapio support.")
|
|
111
158
|
|
|
@@ -120,7 +167,7 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
120
167
|
result: SapioWebhookResult | None = self.handle_any_exception(e)
|
|
121
168
|
if result is not None:
|
|
122
169
|
return result
|
|
123
|
-
return SapioWebhookResult(False
|
|
170
|
+
return SapioWebhookResult(False)
|
|
124
171
|
|
|
125
172
|
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
126
173
|
def handle_any_exception(self, e: Exception) -> SapioWebhookResult | None:
|
|
@@ -136,32 +183,58 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
136
183
|
|
|
137
184
|
def log_info(self, msg: str) -> None:
|
|
138
185
|
"""
|
|
139
|
-
Write an info message to the log. Log destination is stdout. This message will be prepended with
|
|
140
|
-
username and the experiment ID of the experiment they are in, if any.
|
|
186
|
+
Write an info message to the webhook server log. Log destination is stdout. This message will be prepended with
|
|
187
|
+
the user's username and the experiment ID of the experiment they are in, if any.
|
|
141
188
|
"""
|
|
142
|
-
|
|
143
|
-
if self.context.eln_experiment is not None:
|
|
144
|
-
exp_id = self.context.eln_experiment.notebook_experiment_id
|
|
145
|
-
# CR-46333: Add the user's group to the logging message.
|
|
146
|
-
user = self.context.user
|
|
147
|
-
username = user.username
|
|
148
|
-
group_name = user.session_additional_data.current_group_name
|
|
149
|
-
self.logger.info(f"(User: {username}, Group: {group_name}, Experiment: {exp_id}):\n{msg}")
|
|
189
|
+
self.logger.info(self._format_log(msg, "log_info call"))
|
|
150
190
|
|
|
151
191
|
def log_error(self, msg: str) -> None:
|
|
152
192
|
"""
|
|
153
|
-
Write an error message to the log. Log destination is stderr. This message will be prepended with
|
|
154
|
-
username and the experiment ID of the experiment they are in, if any.
|
|
193
|
+
Write an error message to the webhook server log. Log destination is stderr. This message will be prepended with
|
|
194
|
+
the user's username and the experiment ID of the experiment they are in, if any.
|
|
195
|
+
"""
|
|
196
|
+
# PR-46209: Use logger.error instead of logger.info when logging errors.
|
|
197
|
+
self.logger.error(self._format_log(msg, "log_error call"))
|
|
198
|
+
|
|
199
|
+
def log_error_to_webhook_execution_log(self, msg: str) -> None:
|
|
200
|
+
"""
|
|
201
|
+
Write an error message to the platform's webhook execution log. This can be reviewed by navigating to the
|
|
202
|
+
webhook configuration where the webhook that called this function is defined and clicking the "View Log"
|
|
203
|
+
button. From there, select one of the rows for the webhook executions and click "Download Log" from the right
|
|
204
|
+
side table.
|
|
155
205
|
"""
|
|
156
|
-
|
|
206
|
+
messenger = DataMgmtServer.get_messenger(self.user)
|
|
207
|
+
messenger.log_message(VeloxLogMessage(message=self._format_log(msg, "Error occurred during webhook execution."),
|
|
208
|
+
log_level=VeloxLogLevel.ERROR,
|
|
209
|
+
originating_class=self.__class__.__name__))
|
|
210
|
+
|
|
211
|
+
def _format_log(self, msg: str, prefix: str | None = None) -> str:
|
|
212
|
+
"""
|
|
213
|
+
Given a message to log, populate it with some metadata about this particular webhook execution, including
|
|
214
|
+
the group of the user and the invocation type of the webhook call.
|
|
215
|
+
"""
|
|
216
|
+
# If we're able to, provide a link to the location that the error occurred at.
|
|
217
|
+
navigator = SapioNavigationLinker(self.context)
|
|
157
218
|
if self.context.eln_experiment is not None:
|
|
158
|
-
|
|
219
|
+
link = navigator.experiment(self.context.eln_experiment)
|
|
220
|
+
elif self.context.data_record and not self.context.data_record_list:
|
|
221
|
+
link = navigator.data_record(self.context.data_record)
|
|
222
|
+
elif self.context.base_data_record:
|
|
223
|
+
link = navigator.data_record(self.context.base_data_record)
|
|
224
|
+
else:
|
|
225
|
+
link = None
|
|
226
|
+
|
|
227
|
+
message: str = ""
|
|
228
|
+
if prefix:
|
|
229
|
+
message += prefix + "\n"
|
|
230
|
+
message += f"Webhook invocation type: {self.context.end_point_type.display_name}\n"
|
|
231
|
+
message += f"Username: {self.user.username}\n"
|
|
159
232
|
# CR-46333: Add the user's group to the logging message.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
233
|
+
message += f"User group: {self.user.session_additional_data.current_group_name}\n"
|
|
234
|
+
if link:
|
|
235
|
+
message += f"User location: {link}\n"
|
|
236
|
+
message += msg
|
|
237
|
+
return message
|
|
165
238
|
|
|
166
239
|
def is_main_toolbar(self) -> bool:
|
|
167
240
|
"""
|
|
@@ -237,3 +310,51 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
237
310
|
:return: True if this endpoint was invoked as a scheduled action.
|
|
238
311
|
"""
|
|
239
312
|
return self.context.end_point_type == WebhookEndpointType.SCHEDULEDPLUGIN
|
|
313
|
+
|
|
314
|
+
def is_action_button_field(self) -> bool:
|
|
315
|
+
"""
|
|
316
|
+
:return: True if this endpoint was invoked as an action button field.
|
|
317
|
+
"""
|
|
318
|
+
return self.context.end_point_type == WebhookEndpointType.ACTIONDATAFIELD
|
|
319
|
+
|
|
320
|
+
def is_action_text_field(self) -> bool:
|
|
321
|
+
"""
|
|
322
|
+
:return: True if this endpoint was invoked as an action text field.
|
|
323
|
+
"""
|
|
324
|
+
return self.context.end_point_type == WebhookEndpointType.ACTION_TEXT_FIELD
|
|
325
|
+
|
|
326
|
+
def is_custom(self) -> bool:
|
|
327
|
+
"""
|
|
328
|
+
:return: True if this endpoint was invoked from a custom point, such as a custom queue.
|
|
329
|
+
"""
|
|
330
|
+
return self.context.end_point_type == WebhookEndpointType.CUSTOM
|
|
331
|
+
|
|
332
|
+
def is_calendar_event_click_handler(self) -> bool:
|
|
333
|
+
"""
|
|
334
|
+
:return: True if this endpoint was invoked from a calendar event click handler.
|
|
335
|
+
"""
|
|
336
|
+
return self.context.end_point_type == WebhookEndpointType.CALENDAR_EVENT_CLICK_HANDLER
|
|
337
|
+
|
|
338
|
+
def is_eln_menu_grabber(self) -> bool:
|
|
339
|
+
"""
|
|
340
|
+
:return: True if this endpoint was invoked as a notebook entry grabber.
|
|
341
|
+
"""
|
|
342
|
+
return self.context.end_point_type == WebhookEndpointType.NOTEBOOKEXPERIMENTGRABBER
|
|
343
|
+
|
|
344
|
+
def is_conversation_bot(self) -> bool:
|
|
345
|
+
"""
|
|
346
|
+
:return: True if this endpoint was invoked as from a conversation bot.
|
|
347
|
+
"""
|
|
348
|
+
return self.context.end_point_type == WebhookEndpointType.CONVERSATION_BOT
|
|
349
|
+
|
|
350
|
+
def is_multi_data_type_table_toolbar(self) -> bool:
|
|
351
|
+
"""
|
|
352
|
+
:return: True if this endpoint was invoked as a multi data type table toolbar button.
|
|
353
|
+
"""
|
|
354
|
+
return self.context.end_point_type == WebhookEndpointType.REPORTTOOLBAR
|
|
355
|
+
|
|
356
|
+
def can_send_client_callback(self) -> bool:
|
|
357
|
+
"""
|
|
358
|
+
:return: Whether client callbacks and directives can be sent from this webhook's endpoint type.
|
|
359
|
+
"""
|
|
360
|
+
return self.context.is_client_callback_available
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import traceback
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from flask import request
|
|
7
|
+
from sapiopylib.rest.User import SapioUser
|
|
8
|
+
from sapiopylib.rest.WebhookService import AbstractWebhookHandler
|
|
9
|
+
from sapiopylib.rest.pojo.webhook.WebhookResult import SapioWebhookResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AbstractWebserviceHandler(AbstractWebhookHandler):
|
|
13
|
+
"""
|
|
14
|
+
A base class for constructing "webservice" endpoints on your webhook server. These are endpoints that can be
|
|
15
|
+
communicated with by external sources without needing to format the payload JSON in the webhook context format that
|
|
16
|
+
webhook handlers expect.
|
|
17
|
+
|
|
18
|
+
The entire payload JSON is sent to the run method of this class. It is up to the run method to determine how
|
|
19
|
+
this JSON should be parsed. In order to communicate with a Sapio system, a SapioUser object must be able to be
|
|
20
|
+
defined using the payload. Functions have been provided for constructing users with various authentication methods.
|
|
21
|
+
|
|
22
|
+
Since this extends AbstractWebhookHandler, you can still register endpoints from this class in the same way you
|
|
23
|
+
would normal webhook endpoints.
|
|
24
|
+
"""
|
|
25
|
+
def post(self) -> dict[str, Any]:
|
|
26
|
+
"""
|
|
27
|
+
Internal method to be executed to translate incoming requests.
|
|
28
|
+
"""
|
|
29
|
+
# noinspection PyBroadException
|
|
30
|
+
try:
|
|
31
|
+
return self.run(request.json).to_json()
|
|
32
|
+
except Exception:
|
|
33
|
+
print('Error occurred while running webservice custom logic. See traceback.', file=sys.stderr)
|
|
34
|
+
traceback.print_exc()
|
|
35
|
+
return SapioWebhookResult(False, display_text="Error occurred during webservice execution.").to_json()
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def run(self, payload: dict[str, Any]) -> SapioWebhookResult:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def basic_auth(self, url: str, username: str, password: str) -> SapioUser:
|
|
42
|
+
"""
|
|
43
|
+
:param url: The URL of the Sapio system that requests from this user will be sent to.
|
|
44
|
+
Must end in /webservice/api
|
|
45
|
+
:param username: The username to authenticate requests with.
|
|
46
|
+
:param password: The password to authenticate requests with.
|
|
47
|
+
:return: A SapioUser that will authenticate requests using basic auth.
|
|
48
|
+
"""
|
|
49
|
+
return SapioUser(url, self.verify_sapio_cert, self.client_timeout_seconds, username=username, password=password)
|
|
50
|
+
|
|
51
|
+
def api_token_auth(self, url: str, api_token: str) -> SapioUser:
|
|
52
|
+
"""
|
|
53
|
+
:param url: The URL of the Sapio system that requests from this user will be sent to.
|
|
54
|
+
Must end in /webservice/api
|
|
55
|
+
:param api_token: The API token to authenticate requests with.
|
|
56
|
+
:return: A SapioUser that will authenticate requests using an API token.
|
|
57
|
+
"""
|
|
58
|
+
return SapioUser(url, self.verify_sapio_cert, self.client_timeout_seconds, api_token=api_token)
|
|
59
|
+
|
|
60
|
+
def bearer_token_auth(self, url: str, bearer_token: str) -> SapioUser:
|
|
61
|
+
"""
|
|
62
|
+
:param url: The URL of the Sapio system that requests from this user will be sent to.
|
|
63
|
+
Must end in /webservice/api
|
|
64
|
+
:param bearer_token: The bearer token to authenticate requests with.
|
|
65
|
+
:return: A SapioUser that will authenticate requests using a bearer token.
|
|
66
|
+
"""
|
|
67
|
+
return SapioUser(url, self.verify_sapio_cert, self.client_timeout_seconds, bearer_token=bearer_token)
|
|
@@ -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>
|
|
@@ -17,7 +17,8 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
17
17
|
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
|
-
Requires-Dist:
|
|
20
|
+
Requires-Dist: databind>=4.5
|
|
21
|
+
Requires-Dist: sapiopylib>=2024.5.24.210
|
|
21
22
|
Description-Content-Type: text/markdown
|
|
22
23
|
|
|
23
24
|
|
|
@@ -50,6 +51,7 @@ This license does not provide any rights to use any other copyrighted artifacts
|
|
|
50
51
|
## Dependencies
|
|
51
52
|
The following dependencies are required for this package:
|
|
52
53
|
- [sapiopylib - The official Sapio Informatics Platform Python API package.](https://pypi.org/project/sapiopylib/)
|
|
54
|
+
- [databind - Databind is a library inspired by jackson-databind to de-/serialize Python dataclasses.](https://pypi.org/project/databind/)
|
|
53
55
|
|
|
54
56
|
## Getting Help
|
|
55
57
|
If you have a support contract with Sapio Sciences, please use our [technical support channels](https://sapio-sciences.atlassian.net/servicedesk/customer/portals).
|