sapiopycommons 2024.11.7a354__py3-none-any.whl → 2024.11.8a359__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 +83 -532
- sapiopycommons/chem/IndigoMolecules.py +0 -2
- sapiopycommons/chem/Molecules.py +18 -77
- sapiopycommons/datatype/attachment_util.py +10 -11
- sapiopycommons/eln/experiment_handler.py +70 -272
- sapiopycommons/files/complex_data_loader.py +4 -5
- sapiopycommons/files/file_bridge.py +24 -31
- sapiopycommons/files/file_data_handler.py +5 -2
- sapiopycommons/files/file_util.py +9 -59
- sapiopycommons/files/file_validator.py +6 -92
- sapiopycommons/files/file_writer.py +15 -44
- sapiopycommons/general/aliases.py +6 -207
- sapiopycommons/general/custom_report_util.py +37 -212
- sapiopycommons/general/exceptions.py +8 -21
- sapiopycommons/general/popup_util.py +0 -21
- sapiopycommons/general/time_util.py +2 -8
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +97 -481
- sapiopycommons/rules/eln_rule_handler.py +25 -34
- sapiopycommons/rules/on_save_rule_handler.py +31 -34
- sapiopycommons/webhook/webhook_handlers.py +42 -201
- {sapiopycommons-2024.11.7a354.dist-info → sapiopycommons-2024.11.8a359.dist-info}/METADATA +2 -4
- sapiopycommons-2024.11.8a359.dist-info/RECORD +38 -0
- sapiopycommons/callbacks/field_builder.py +0 -537
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +0 -60
- sapiopycommons/customreport/custom_report_builder.py +0 -130
- sapiopycommons/customreport/term_builder.py +0 -299
- sapiopycommons/datatype/data_fields.py +0 -61
- sapiopycommons/datatype/pseudo_data_types.py +0 -440
- sapiopycommons/eln/experiment_report_util.py +0 -653
- sapiopycommons/files/file_bridge_handler.py +0 -340
- sapiopycommons/flowcyto/flow_cyto.py +0 -77
- sapiopycommons/flowcyto/flowcyto_data.py +0 -75
- sapiopycommons/general/accession_service.py +0 -375
- sapiopycommons/general/audit_log.py +0 -189
- sapiopycommons/general/sapio_links.py +0 -50
- sapiopycommons/multimodal/multimodal.py +0 -146
- sapiopycommons/multimodal/multimodal_data.py +0 -489
- sapiopycommons/processtracking/custom_workflow_handler.py +0 -406
- sapiopycommons/sftpconnect/__init__.py +0 -0
- sapiopycommons/sftpconnect/sftp_builder.py +0 -69
- sapiopycommons/webhook/webhook_context.py +0 -39
- sapiopycommons/webhook/webservice_handlers.py +0 -67
- sapiopycommons-2024.11.7a354.dist-info/RECORD +0 -59
- {sapiopycommons-2024.11.7a354.dist-info → sapiopycommons-2024.11.8a359.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.11.7a354.dist-info → sapiopycommons-2024.11.8a359.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import Any
|
|
5
|
-
from weakref import WeakValueDictionary
|
|
6
|
-
|
|
7
|
-
from sapiopylib.rest.User import SapioUser
|
|
8
|
-
|
|
9
|
-
_STR_JAVA_TYPE = "java.lang.String"
|
|
10
|
-
_INT_JAVA_TYPE = "java.lang.Integer"
|
|
11
|
-
_BOOL_JAVA_TYPE = "java.lang.Boolean"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class AbstractAccessionServiceOperator(ABC):
|
|
15
|
-
"""
|
|
16
|
-
Abstract class to define an accession service operator.
|
|
17
|
-
The default one in sapiopycommon only includes the out of box operators.
|
|
18
|
-
More can be added via java plugins for global operators.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
@property
|
|
22
|
-
@abstractmethod
|
|
23
|
-
def op_class_name(self) -> str:
|
|
24
|
-
pass
|
|
25
|
-
|
|
26
|
-
@property
|
|
27
|
-
@abstractmethod
|
|
28
|
-
def op_param_value_list(self) -> list[Any] | None:
|
|
29
|
-
pass
|
|
30
|
-
|
|
31
|
-
@property
|
|
32
|
-
@abstractmethod
|
|
33
|
-
def op_param_class_name_list(self) -> list[str] | None:
|
|
34
|
-
pass
|
|
35
|
-
|
|
36
|
-
@property
|
|
37
|
-
@abstractmethod
|
|
38
|
-
def default_accessor_name(self) -> str:
|
|
39
|
-
pass
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class AccessionWithPrefixSuffix(AbstractAccessionServiceOperator):
|
|
43
|
-
"""
|
|
44
|
-
Local operator for accessioning prefix and suffix format.
|
|
45
|
-
"""
|
|
46
|
-
_prefix: str | None
|
|
47
|
-
_suffix: str | None
|
|
48
|
-
_num_of_digits: int | None
|
|
49
|
-
_start_num: int
|
|
50
|
-
_strict_mode: bool
|
|
51
|
-
|
|
52
|
-
@property
|
|
53
|
-
def prefix(self):
|
|
54
|
-
return self._prefix
|
|
55
|
-
|
|
56
|
-
@property
|
|
57
|
-
def suffix(self):
|
|
58
|
-
return self._suffix
|
|
59
|
-
|
|
60
|
-
@property
|
|
61
|
-
def num_of_digits(self):
|
|
62
|
-
return self._num_of_digits
|
|
63
|
-
|
|
64
|
-
@property
|
|
65
|
-
def start_num(self):
|
|
66
|
-
return self._start_num
|
|
67
|
-
|
|
68
|
-
@property
|
|
69
|
-
def strict_mode(self):
|
|
70
|
-
return self._strict_mode
|
|
71
|
-
|
|
72
|
-
def __init__(self, prefix: str | None, suffix: str | None, num_of_digits: int | None = None,
|
|
73
|
-
start_num: int = 1, strict_mode: bool = False):
|
|
74
|
-
if prefix is None:
|
|
75
|
-
prefix = ""
|
|
76
|
-
if suffix is None:
|
|
77
|
-
suffix = ""
|
|
78
|
-
self._prefix = prefix
|
|
79
|
-
self._suffix = suffix
|
|
80
|
-
self._num_of_digits = num_of_digits
|
|
81
|
-
self._start_num = start_num
|
|
82
|
-
self._strict_mode = strict_mode
|
|
83
|
-
|
|
84
|
-
@property
|
|
85
|
-
def op_param_value_list(self):
|
|
86
|
-
return [self._prefix, self._suffix, self._num_of_digits, self._start_num, self._strict_mode]
|
|
87
|
-
|
|
88
|
-
@property
|
|
89
|
-
def op_param_class_name_list(self):
|
|
90
|
-
return [_STR_JAVA_TYPE, _STR_JAVA_TYPE, _INT_JAVA_TYPE, _INT_JAVA_TYPE, _BOOL_JAVA_TYPE]
|
|
91
|
-
|
|
92
|
-
@property
|
|
93
|
-
def op_class_name(self):
|
|
94
|
-
return "com.velox.accessionservice.operators.AccessionWithPrefixSuffix"
|
|
95
|
-
|
|
96
|
-
@property
|
|
97
|
-
def default_accessor_name(self):
|
|
98
|
-
return "PREFIX_AND_SUFFIX" + "(" + self.prefix + "," + self.suffix + ")";
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
class AccessionGlobalPrefixSuffix(AbstractAccessionServiceOperator):
|
|
102
|
-
"""
|
|
103
|
-
Global operator for accessioning prefix and suffix format.
|
|
104
|
-
"""
|
|
105
|
-
_prefix: str | None
|
|
106
|
-
_suffix: str | None
|
|
107
|
-
_num_of_digits: int | None
|
|
108
|
-
_start_num: int
|
|
109
|
-
_strict_mode: bool
|
|
110
|
-
|
|
111
|
-
@property
|
|
112
|
-
def prefix(self):
|
|
113
|
-
return self._prefix
|
|
114
|
-
|
|
115
|
-
@property
|
|
116
|
-
def suffix(self):
|
|
117
|
-
return self._suffix
|
|
118
|
-
|
|
119
|
-
@property
|
|
120
|
-
def num_of_digits(self):
|
|
121
|
-
return self._num_of_digits
|
|
122
|
-
|
|
123
|
-
@property
|
|
124
|
-
def start_num(self):
|
|
125
|
-
return self._start_num
|
|
126
|
-
|
|
127
|
-
@property
|
|
128
|
-
def strict_mode(self):
|
|
129
|
-
return self._strict_mode
|
|
130
|
-
|
|
131
|
-
def __init__(self, prefix: str | None, suffix: str | None, num_of_digits: int | None = None,
|
|
132
|
-
start_num: int = 1, strict_mode: bool = False):
|
|
133
|
-
if prefix is None:
|
|
134
|
-
prefix = ""
|
|
135
|
-
if suffix is None:
|
|
136
|
-
suffix = ""
|
|
137
|
-
self._prefix = prefix
|
|
138
|
-
self._suffix = suffix
|
|
139
|
-
self._num_of_digits = num_of_digits
|
|
140
|
-
self._start_num = start_num
|
|
141
|
-
self._strict_mode = strict_mode
|
|
142
|
-
|
|
143
|
-
@property
|
|
144
|
-
def op_param_value_list(self):
|
|
145
|
-
return [self._prefix, self._suffix, self._num_of_digits, self._start_num, self._strict_mode]
|
|
146
|
-
|
|
147
|
-
@property
|
|
148
|
-
def op_param_class_name_list(self):
|
|
149
|
-
return [_STR_JAVA_TYPE, _STR_JAVA_TYPE, _INT_JAVA_TYPE, _INT_JAVA_TYPE, _BOOL_JAVA_TYPE]
|
|
150
|
-
|
|
151
|
-
@property
|
|
152
|
-
def op_class_name(self):
|
|
153
|
-
return "com.velox.accessionservice.operators.sapio.AccessionGlobalPrefixSuffix"
|
|
154
|
-
|
|
155
|
-
@property
|
|
156
|
-
def default_accessor_name(self):
|
|
157
|
-
return "PREFIX_AND_SUFFIX" + "(" + self._prefix + "," + self._suffix + ")"
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
class AccessionNextBarcode(AbstractAccessionServiceOperator):
|
|
161
|
-
"""
|
|
162
|
-
From Java description:
|
|
163
|
-
This will start accessioning at the getNextBarcode() when there's no system preference to be backward compatible.
|
|
164
|
-
However, once it completes setting the first ID, it will start increment by its own preference and disregards getNextBarcode().
|
|
165
|
-
|
|
166
|
-
Recommend using AccessionServiceBasicManager to accession next barcode.
|
|
167
|
-
To avoid ambiguity in preference cache.
|
|
168
|
-
|
|
169
|
-
This should not be used unless we are using something legacy such as plate mapping template record creation
|
|
170
|
-
(Note: not 3D plating, I'm talking about the older aliquoter).
|
|
171
|
-
"""
|
|
172
|
-
|
|
173
|
-
@property
|
|
174
|
-
def op_param_value_list(self):
|
|
175
|
-
return []
|
|
176
|
-
|
|
177
|
-
@property
|
|
178
|
-
def op_param_class_name_list(self):
|
|
179
|
-
return []
|
|
180
|
-
|
|
181
|
-
@property
|
|
182
|
-
def op_class_name(self):
|
|
183
|
-
return "com.velox.accessionservice.operators.sapio.AccessionNextBarcode"
|
|
184
|
-
|
|
185
|
-
@property
|
|
186
|
-
def default_accessor_name(self):
|
|
187
|
-
return "Barcode"
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
class AccessionRequestId(AbstractAccessionServiceOperator):
|
|
191
|
-
"""
|
|
192
|
-
This class implements the accessioning operator for com.velox.sapioutils.shared.managers.DataRecordUtilManager.getNextRequestId()
|
|
193
|
-
and getNextRequestId(int numberOfCharacters).
|
|
194
|
-
|
|
195
|
-
Operation: For 4 characters start with A001, increment by 1 until A999. Then We use B001.
|
|
196
|
-
After Z999 we start with AA01 until we get to AA99, etc.
|
|
197
|
-
|
|
198
|
-
Exception: Skips I and O to prevent confusions with 1 and 0 when incrementing letters.
|
|
199
|
-
|
|
200
|
-
Properties:
|
|
201
|
-
numberOfCharacters: Number of characters maximum in the request ID.
|
|
202
|
-
accessorName: This is a legacy variable from drum.getNextIdListByMapName(), which allows setting different "accessorName" from old system. We need this for compability patch for converting these to the new preference format.
|
|
203
|
-
"""
|
|
204
|
-
_num_of_characters: int
|
|
205
|
-
_accessor_name: str
|
|
206
|
-
|
|
207
|
-
@property
|
|
208
|
-
def num_of_characters(self):
|
|
209
|
-
return self._num_of_characters
|
|
210
|
-
|
|
211
|
-
@property
|
|
212
|
-
def accessor_name(self):
|
|
213
|
-
return self._accessor_name
|
|
214
|
-
|
|
215
|
-
def __init__(self, num_of_characters: int = 4, accessor_name: str = None):
|
|
216
|
-
self._num_of_characters = num_of_characters
|
|
217
|
-
if not accessor_name:
|
|
218
|
-
accessor_name = self.default_accessor_name
|
|
219
|
-
self._accessor_name = accessor_name
|
|
220
|
-
|
|
221
|
-
@property
|
|
222
|
-
def op_class_name(self):
|
|
223
|
-
return "com.velox.accessionservice.operators.sapio.AccessionRequestId"
|
|
224
|
-
|
|
225
|
-
@property
|
|
226
|
-
def op_param_value_list(self):
|
|
227
|
-
return [self._num_of_characters, self._accessor_name]
|
|
228
|
-
|
|
229
|
-
@property
|
|
230
|
-
def op_param_class_name_list(self):
|
|
231
|
-
return [_INT_JAVA_TYPE, _STR_JAVA_TYPE]
|
|
232
|
-
|
|
233
|
-
@property
|
|
234
|
-
def default_accessor_name(self):
|
|
235
|
-
return "SapioNextRequestIdMap"
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
class AccessionServiceDescriptor:
|
|
239
|
-
"""
|
|
240
|
-
Describes a single accession service's accessioning request
|
|
241
|
-
|
|
242
|
-
Attributes:
|
|
243
|
-
opClassName: The accession service operator class name as in Java
|
|
244
|
-
opParamValueList: Ordered list of parameter values to construct the accession service operator.
|
|
245
|
-
opParamClassNameList: Ordered list of FQCN of java classes in order of parameter value list.
|
|
246
|
-
dataTypeName: The data type to accession. Should be blank if opClassName resolves to a global operator.
|
|
247
|
-
dataFieldName: The data field to accession. Should be blank if opClassName resolves to a global operator.
|
|
248
|
-
accessorName: The accessor cache name to be used for accessioning.
|
|
249
|
-
numIds: The number of IDs to accession.
|
|
250
|
-
"""
|
|
251
|
-
op: AbstractAccessionServiceOperator
|
|
252
|
-
dataTypeName: str | None
|
|
253
|
-
dataFieldName: str | None
|
|
254
|
-
accessorName: str
|
|
255
|
-
numIds: int
|
|
256
|
-
|
|
257
|
-
def __init__(self, accessor_name: str, op: AbstractAccessionServiceOperator, num_ids: int,
|
|
258
|
-
data_type_name: str | None, data_field_name: str | None):
|
|
259
|
-
self.accessorName = accessor_name
|
|
260
|
-
self.op = op
|
|
261
|
-
self.dataTypeName = data_type_name
|
|
262
|
-
self.dataFieldName = data_field_name
|
|
263
|
-
self.numIds = num_ids
|
|
264
|
-
|
|
265
|
-
def to_json(self):
|
|
266
|
-
return {
|
|
267
|
-
"opClassName": self.op.op_class_name,
|
|
268
|
-
"opParamValueList": self.op.op_param_value_list,
|
|
269
|
-
"opParamClassNameList": self.op.op_param_class_name_list,
|
|
270
|
-
"accessorName": self.accessorName,
|
|
271
|
-
"numIds": self.numIds,
|
|
272
|
-
"dataTypeName": self.dataTypeName,
|
|
273
|
-
"dataFieldName": self.dataFieldName
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
class AccessionService:
|
|
278
|
-
"""
|
|
279
|
-
Provides Sapio Foundations Accession Service functionalities.
|
|
280
|
-
"""
|
|
281
|
-
_user: SapioUser
|
|
282
|
-
|
|
283
|
-
__instances: WeakValueDictionary[SapioUser, AccessionService] = WeakValueDictionary()
|
|
284
|
-
__initialized: bool
|
|
285
|
-
|
|
286
|
-
@property
|
|
287
|
-
def user(self) -> SapioUser:
|
|
288
|
-
return self._user
|
|
289
|
-
|
|
290
|
-
def __new__(cls, user: SapioUser):
|
|
291
|
-
"""
|
|
292
|
-
Observes singleton pattern per record model manager object.
|
|
293
|
-
|
|
294
|
-
:param user: The user that will make the webservice request to the application.
|
|
295
|
-
"""
|
|
296
|
-
obj = cls.__instances.get(user)
|
|
297
|
-
if not obj:
|
|
298
|
-
obj = object.__new__(cls)
|
|
299
|
-
obj.__initialized = False
|
|
300
|
-
cls.__instances[user] = obj
|
|
301
|
-
return obj
|
|
302
|
-
|
|
303
|
-
def __init__(self, user: SapioUser):
|
|
304
|
-
if self.__initialized:
|
|
305
|
-
return
|
|
306
|
-
self._user = user
|
|
307
|
-
self.__initialized = True
|
|
308
|
-
|
|
309
|
-
def accession_with_config(self, data_type_name: str, data_field_name: str, num_ids: int) -> list[str]:
|
|
310
|
-
"""
|
|
311
|
-
Accession with Configuration Manager => Accession Service configuration (This is not visible to regular users in SaaS)
|
|
312
|
-
"""
|
|
313
|
-
payload = {
|
|
314
|
-
"dataTypeName": data_type_name,
|
|
315
|
-
"dataFieldName": data_field_name,
|
|
316
|
-
"numIds": num_ids
|
|
317
|
-
}
|
|
318
|
-
response = self.user.plugin_post("accessionservice/accession_with_config", payload=payload)
|
|
319
|
-
self.user.raise_for_status(response)
|
|
320
|
-
return list(response.json())
|
|
321
|
-
|
|
322
|
-
def accession_in_batch(self, descriptor: AccessionServiceDescriptor) -> list[str]:
|
|
323
|
-
"""
|
|
324
|
-
This is the most flexible way to make use of accession service: directly via a descriptor object.
|
|
325
|
-
"""
|
|
326
|
-
payload = descriptor.to_json()
|
|
327
|
-
response = self.user.plugin_post("accessionservice/accession", payload=payload)
|
|
328
|
-
self.user.raise_for_status(response)
|
|
329
|
-
return list(response.json())
|
|
330
|
-
|
|
331
|
-
def accession_next_request_id_list(self, num_of_characters: int, num_ids: int) -> list[str]:
|
|
332
|
-
"""
|
|
333
|
-
Accession Request ID by old LIMS format. This is usually deprecated today.
|
|
334
|
-
:param num_of_characters: Number of characters minimum in request ID.
|
|
335
|
-
:param num_ids: Number of request IDs to accession.
|
|
336
|
-
"""
|
|
337
|
-
op = AccessionRequestId(num_of_characters)
|
|
338
|
-
descriptor = AccessionServiceDescriptor(op.default_accessor_name, op, num_ids, None, None)
|
|
339
|
-
return self.accession_in_batch(descriptor)
|
|
340
|
-
|
|
341
|
-
def get_affixed_id_in_batch(self, data_type_name: str, data_field_name: str, num_ids: int, prefix: str | None,
|
|
342
|
-
suffix: str | None, num_digits: int | None, start_num: int = 1) -> list[str]:
|
|
343
|
-
"""
|
|
344
|
-
Get the batch affixed IDs that are maximal in cache and contiguious for a particular datatype.datafield under a given format.
|
|
345
|
-
:param data_type_name: The datatype name to look for max ID
|
|
346
|
-
:param data_field_name: The datafield name to look for max ID
|
|
347
|
-
:param num_ids: The number of IDs to accession.
|
|
348
|
-
:param prefix: leave it empty string "" if no prefix. Otherwise, specifies the prefix of ID.
|
|
349
|
-
:param suffix: leave it empty string "" if no suffix. Otherwise, specifies the suffix of ID.
|
|
350
|
-
:param num_digits: None if unlimited with no leading zeros.
|
|
351
|
-
:param start_num The number to begin accessioning if this is the first time.
|
|
352
|
-
:return:
|
|
353
|
-
"""
|
|
354
|
-
op = AccessionWithPrefixSuffix(prefix, suffix, num_digits, start_num)
|
|
355
|
-
descriptor = AccessionServiceDescriptor(op.default_accessor_name, op, num_ids, data_type_name, data_field_name)
|
|
356
|
-
return self.accession_in_batch(descriptor)
|
|
357
|
-
|
|
358
|
-
def get_global_affixed_id_in_batch(
|
|
359
|
-
self, num_ids: int, prefix: str | None, suffix: str | None, num_digits: int | None, start_num: int = 1) -> list[str]:
|
|
360
|
-
"""
|
|
361
|
-
Get the next numOfIds affixed IDs using system preference cache that's maximum across all datatype and datafields and maximal for the format.
|
|
362
|
-
This method allows users to customize a start number instead of always starting at 1.
|
|
363
|
-
:param num_ids: The number of IDs to accession.
|
|
364
|
-
:param prefix: leave it empty string "" if no prefix. Otherwise, specifies the prefix of ID.
|
|
365
|
-
:param suffix: leave it empty string "" if no suffix. Otherwise, specifies the suffix of ID.
|
|
366
|
-
:param num_digits: None if unlimited with no leading zeros.
|
|
367
|
-
:param start_num The number to begin accessioning if this is the first time.
|
|
368
|
-
"""
|
|
369
|
-
op: AbstractAccessionServiceOperator
|
|
370
|
-
if not prefix and not suffix:
|
|
371
|
-
op = AccessionNextBarcode()
|
|
372
|
-
else:
|
|
373
|
-
op = AccessionGlobalPrefixSuffix(prefix, suffix, num_digits, start_num)
|
|
374
|
-
descriptor = AccessionServiceDescriptor(op.default_accessor_name, op, num_ids, None, None)
|
|
375
|
-
return self.accession_in_batch(descriptor)
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
|
|
3
|
-
from sapiopylib.rest.User import SapioUser
|
|
4
|
-
from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReportCriteria
|
|
5
|
-
|
|
6
|
-
from sapiopycommons.customreport.column_builder import ColumnBuilder
|
|
7
|
-
from sapiopycommons.customreport.term_builder import TermBuilder
|
|
8
|
-
from sapiopycommons.datatype.pseudo_data_types import AuditLogPseudoDef
|
|
9
|
-
from sapiopycommons.general.aliases import RecordIdentifier, AliasUtil, UserIdentifier, FieldIdentifier, FieldValue
|
|
10
|
-
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class EventType(Enum):
|
|
14
|
-
"""An enum to represent the possible event type values with the event type column in the audit log table."""
|
|
15
|
-
ADD = 0
|
|
16
|
-
DELETE = 1
|
|
17
|
-
MODIFY = 2
|
|
18
|
-
INFO = 3
|
|
19
|
-
ERROR = 4
|
|
20
|
-
WARNING = 5
|
|
21
|
-
IMPORT = 6
|
|
22
|
-
GENERATE = 7
|
|
23
|
-
EXPORT = 8
|
|
24
|
-
ADDREF = 9
|
|
25
|
-
REMOVEREF = 10
|
|
26
|
-
ESIGNATURE = 11
|
|
27
|
-
ROLEASSIGNMENT = 12
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class AuditLogEntry:
|
|
31
|
-
__event_type: EventType
|
|
32
|
-
__date: int
|
|
33
|
-
__data_type_name: str
|
|
34
|
-
__record_id: int
|
|
35
|
-
__description: str
|
|
36
|
-
__users_login_name: str
|
|
37
|
-
__comment: str
|
|
38
|
-
__data_record_name: str
|
|
39
|
-
__data_field_name: str
|
|
40
|
-
__original_value: str
|
|
41
|
-
__new_value: str
|
|
42
|
-
|
|
43
|
-
@property
|
|
44
|
-
def event_type(self) -> EventType:
|
|
45
|
-
return self.__event_type
|
|
46
|
-
|
|
47
|
-
@property
|
|
48
|
-
def date(self) -> int:
|
|
49
|
-
return self.__date
|
|
50
|
-
|
|
51
|
-
@property
|
|
52
|
-
def data_type_name(self) -> str:
|
|
53
|
-
return self.__data_type_name
|
|
54
|
-
|
|
55
|
-
@property
|
|
56
|
-
def record_id(self) -> int:
|
|
57
|
-
return self.__record_id
|
|
58
|
-
|
|
59
|
-
@property
|
|
60
|
-
def description(self) -> str:
|
|
61
|
-
return self.__description
|
|
62
|
-
|
|
63
|
-
@property
|
|
64
|
-
def users_login_name(self) -> str:
|
|
65
|
-
return self.__users_login_name
|
|
66
|
-
|
|
67
|
-
@property
|
|
68
|
-
def comment(self) -> str:
|
|
69
|
-
return self.__comment
|
|
70
|
-
|
|
71
|
-
@property
|
|
72
|
-
def data_record_name(self) -> str:
|
|
73
|
-
return self.__data_record_name
|
|
74
|
-
|
|
75
|
-
@property
|
|
76
|
-
def data_field_name(self) -> str:
|
|
77
|
-
return self.__data_field_name
|
|
78
|
-
|
|
79
|
-
@property
|
|
80
|
-
def original_value(self) -> str:
|
|
81
|
-
return self.__original_value
|
|
82
|
-
|
|
83
|
-
@property
|
|
84
|
-
def new_value(self) -> str:
|
|
85
|
-
return self.__new_value
|
|
86
|
-
|
|
87
|
-
def __init__(self, report_row: dict[str, FieldValue]):
|
|
88
|
-
self.__event_type = EventType((report_row[AuditLogPseudoDef.EVENT_TYPE__FIELD_NAME.field_name]))
|
|
89
|
-
self.__date = report_row[AuditLogPseudoDef.TIME_STAMP__FIELD_NAME.field_name]
|
|
90
|
-
self.__data_type_name = report_row[AuditLogPseudoDef.DATA_TYPE_NAME__FIELD_NAME.field_name]
|
|
91
|
-
self.__record_id = report_row[AuditLogPseudoDef.RECORD_ID__FIELD_NAME.field_name]
|
|
92
|
-
self.__description = report_row[AuditLogPseudoDef.DESCRIPTION__FIELD_NAME.field_name]
|
|
93
|
-
self.__users_login_name = report_row[AuditLogPseudoDef.USER_NAME__FIELD_NAME.field_name]
|
|
94
|
-
self.__comment = report_row[AuditLogPseudoDef.USER_COMMENT__FIELD_NAME.field_name]
|
|
95
|
-
self.__data_record_name = report_row[AuditLogPseudoDef.RECORD_NAME__FIELD_NAME]
|
|
96
|
-
self.__data_field_name = report_row[AuditLogPseudoDef.DATA_FIELD_NAME__FIELD_NAME.field_name]
|
|
97
|
-
self.__original_value = report_row[AuditLogPseudoDef.ORIGINAL_VALUE__FIELD_NAME.field_name]
|
|
98
|
-
self.__new_value = report_row[AuditLogPseudoDef.NEW_VALUE__FIELD_NAME.field_name]
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
class AuditLogUtil:
|
|
102
|
-
user: SapioUser
|
|
103
|
-
|
|
104
|
-
def __init__(self, context: UserIdentifier):
|
|
105
|
-
self.user = AliasUtil.to_sapio_user(context)
|
|
106
|
-
|
|
107
|
-
@staticmethod
|
|
108
|
-
def report_columns() -> list[ReportColumn]:
|
|
109
|
-
return [
|
|
110
|
-
ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.EVENT_TYPE__FIELD_NAME),
|
|
111
|
-
ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.TIME_STAMP__FIELD_NAME),
|
|
112
|
-
ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.DATA_TYPE_NAME__FIELD_NAME),
|
|
113
|
-
ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.RECORD_ID__FIELD_NAME),
|
|
114
|
-
ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.DESCRIPTION__FIELD_NAME),
|
|
115
|
-
ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.USER_NAME__FIELD_NAME),
|
|
116
|
-
ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.USER_COMMENT__FIELD_NAME),
|
|
117
|
-
ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.RECORD_NAME__FIELD_NAME),
|
|
118
|
-
ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.DATA_FIELD_NAME__FIELD_NAME),
|
|
119
|
-
ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.ORIGINAL_VALUE__FIELD_NAME),
|
|
120
|
-
ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.NEW_VALUE__FIELD_NAME)
|
|
121
|
-
]
|
|
122
|
-
|
|
123
|
-
@staticmethod
|
|
124
|
-
def create_data_record_audit_log_report(records: list[RecordIdentifier],
|
|
125
|
-
fields: list[FieldIdentifier] | None = None) -> CustomReportCriteria:
|
|
126
|
-
"""
|
|
127
|
-
This method creates a CustomReportCriteria object for running an audit log query based on data records.
|
|
128
|
-
|
|
129
|
-
Creates a CustomReportCriteria object with a query term based on the record ids/records passed into the method.
|
|
130
|
-
Optionally, the fields parameter can be populated to limit the search to particular fields. If the fields
|
|
131
|
-
parameter is not populated, the search will include results for all field changes.
|
|
132
|
-
|
|
133
|
-
:param records: The DataRecords, RecordModels, or record ids to base the search on.
|
|
134
|
-
:param fields: The data field names to include changes for.
|
|
135
|
-
:return: The constructed CustomReportCriteria object, which can be used to run a report on the audit log.
|
|
136
|
-
"""
|
|
137
|
-
# Build the raw report term querying for any entry with a matching record ID value to the record ID's
|
|
138
|
-
# passed in.
|
|
139
|
-
record_ids = AliasUtil.to_record_ids(records)
|
|
140
|
-
root_term = TermBuilder.is_term(AuditLogPseudoDef.DATA_TYPE_NAME,
|
|
141
|
-
AuditLogPseudoDef.RECORD_ID__FIELD_NAME,
|
|
142
|
-
record_ids)
|
|
143
|
-
|
|
144
|
-
# If the user passed in any specific fields, then we should limit the query to those fields.
|
|
145
|
-
if fields:
|
|
146
|
-
fields: list[str] = AliasUtil.to_data_field_names(fields)
|
|
147
|
-
field_term = TermBuilder.is_term(AuditLogPseudoDef.DATA_TYPE_NAME,
|
|
148
|
-
AuditLogPseudoDef.DATA_FIELD_NAME__FIELD_NAME,
|
|
149
|
-
fields)
|
|
150
|
-
root_term = TermBuilder.and_terms(root_term, field_term)
|
|
151
|
-
|
|
152
|
-
return CustomReportCriteria(AuditLogUtil.report_columns(), root_term)
|
|
153
|
-
|
|
154
|
-
def run_data_record_audit_log_report(self, records: list[RecordIdentifier],
|
|
155
|
-
fields: list[FieldIdentifier] | None = None) \
|
|
156
|
-
-> dict[RecordIdentifier, list[AuditLogEntry]]:
|
|
157
|
-
"""
|
|
158
|
-
This method runs a custom report for changes made to the given data records using the audit log.
|
|
159
|
-
See "create_data_record_audit_log_report" for more details about the data record audit log report.
|
|
160
|
-
|
|
161
|
-
:param records: The DataRecords, RecordModels, or record ids to base the search on.
|
|
162
|
-
:param fields: The data field names to include changes for.
|
|
163
|
-
:return: A dictionary where the keys are the record identifiers passed in, and the values are a list of
|
|
164
|
-
AuditLogEntry objects which match the record id value of those records.
|
|
165
|
-
"""
|
|
166
|
-
fields: list[str] = AliasUtil.to_data_field_names(fields)
|
|
167
|
-
# First, we must build our report criteria for running the Custom Report.
|
|
168
|
-
criteria = AuditLogUtil.create_data_record_audit_log_report(records, fields)
|
|
169
|
-
|
|
170
|
-
# Then we must run the custom report using that criteria.
|
|
171
|
-
raw_report_data: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, criteria)
|
|
172
|
-
|
|
173
|
-
# This section will prepare a map matching the original RecordIdentifier by record id.
|
|
174
|
-
# This is because the audit log entries will have record ids, but we want the keys in our result map
|
|
175
|
-
# to match the record identifiers that the user passed in, for convenience.
|
|
176
|
-
record_identifier_mapping: dict[int, RecordIdentifier] = dict()
|
|
177
|
-
for record in records:
|
|
178
|
-
record_id = AliasUtil.to_record_id(record)
|
|
179
|
-
record_identifier_mapping[record_id] = record
|
|
180
|
-
|
|
181
|
-
# Finally, we compile our audit data into a map where the keys are the record identifiers passed in,
|
|
182
|
-
# and the value is a list of applicable audit log entries.
|
|
183
|
-
final_audit_data: dict[RecordIdentifier, list[AuditLogEntry]] = dict()
|
|
184
|
-
for audit_entry_data in raw_report_data:
|
|
185
|
-
audit_entry: AuditLogEntry = AuditLogEntry(audit_entry_data)
|
|
186
|
-
identifier: RecordIdentifier = record_identifier_mapping.get(audit_entry.record_id)
|
|
187
|
-
final_audit_data.setdefault(identifier, []).append(audit_entry)
|
|
188
|
-
|
|
189
|
-
return final_audit_data
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
from sapiopylib.rest.User import SapioUser
|
|
2
|
-
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
3
|
-
|
|
4
|
-
from sapiopycommons.general.aliases import RecordIdentifier, ExperimentIdentifier, AliasUtil, DataTypeIdentifier
|
|
5
|
-
from sapiopycommons.general.exceptions import SapioException
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class SapioNavigationLinker:
|
|
9
|
-
"""
|
|
10
|
-
Given a URL to a system's webservice API (example: https://company.exemplareln.com/webservice/api), construct
|
|
11
|
-
URLs for navigation links to various locations in the system.
|
|
12
|
-
"""
|
|
13
|
-
base_url: str
|
|
14
|
-
|
|
15
|
-
def __init__(self, url: str | SapioUser | SapioWebhookContext):
|
|
16
|
-
"""
|
|
17
|
-
:param url: A user or context object that is being used to send requests to a Sapio system, or a URL to a
|
|
18
|
-
system's webservice API.
|
|
19
|
-
"""
|
|
20
|
-
if isinstance(url, SapioWebhookContext):
|
|
21
|
-
url = url.user.url
|
|
22
|
-
elif isinstance(url, SapioUser):
|
|
23
|
-
url = url.url
|
|
24
|
-
self.base_url = url.rstrip("/").replace('webservice/api', 'veloxClient')
|
|
25
|
-
|
|
26
|
-
def data_record(self, record_identifier: RecordIdentifier, data_type_name: DataTypeIdentifier | None = None) -> str:
|
|
27
|
-
"""
|
|
28
|
-
:param record_identifier: An object that can be used to identify a record in the system, be that a record ID,
|
|
29
|
-
a data record, or a record model.
|
|
30
|
-
:param data_type_name: If the provided record identifier is a record ID, then the data type name of the record
|
|
31
|
-
must be provided in this parameter. Otherwise, this parameter is ignored.
|
|
32
|
-
:return: A URL for navigating to the input record.
|
|
33
|
-
"""
|
|
34
|
-
record_id: int = AliasUtil.to_record_id(record_identifier)
|
|
35
|
-
if data_type_name:
|
|
36
|
-
data_type_name = AliasUtil.to_data_type_name(data_type_name)
|
|
37
|
-
if not isinstance(record_identifier, int):
|
|
38
|
-
data_type_name = AliasUtil.to_data_type_name(record_identifier)
|
|
39
|
-
if not data_type_name:
|
|
40
|
-
raise SapioException("Unable to create a data record link without a data type name. "
|
|
41
|
-
"Only a record ID was provided.")
|
|
42
|
-
return self.base_url + f"/#dataType={data_type_name};recordId={record_id};view=dataRecord"
|
|
43
|
-
|
|
44
|
-
def experiment(self, experiment: ExperimentIdentifier) -> str:
|
|
45
|
-
"""
|
|
46
|
-
:param experiment: An object that can be used to identify an experiment in the system, be that an experiment
|
|
47
|
-
object, experiment protocol, or a notebook ID.
|
|
48
|
-
:return: A URL for navigating to the input experiment.
|
|
49
|
-
"""
|
|
50
|
-
return self.base_url + f"/#notebookExperimentId={AliasUtil.to_notebook_id(experiment)};view=eln"
|