sapiopycommons 2024.3.18a156__py3-none-any.whl → 2025.1.17a402__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/__init__.py +0 -0
- sapiopycommons/callbacks/callback_util.py +2041 -0
- sapiopycommons/callbacks/field_builder.py +545 -0
- sapiopycommons/chem/IndigoMolecules.py +52 -5
- sapiopycommons/chem/Molecules.py +114 -30
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +60 -0
- sapiopycommons/customreport/custom_report_builder.py +137 -0
- sapiopycommons/customreport/term_builder.py +315 -0
- sapiopycommons/datatype/attachment_util.py +17 -15
- sapiopycommons/datatype/data_fields.py +61 -0
- sapiopycommons/datatype/pseudo_data_types.py +440 -0
- sapiopycommons/eln/experiment_handler.py +390 -90
- sapiopycommons/eln/experiment_report_util.py +649 -0
- sapiopycommons/eln/plate_designer.py +152 -0
- sapiopycommons/files/complex_data_loader.py +31 -0
- sapiopycommons/files/file_bridge.py +153 -25
- sapiopycommons/files/file_bridge_handler.py +555 -0
- sapiopycommons/files/file_data_handler.py +633 -0
- sapiopycommons/files/file_util.py +270 -158
- sapiopycommons/files/file_validator.py +569 -0
- sapiopycommons/files/file_writer.py +377 -0
- sapiopycommons/flowcyto/flow_cyto.py +77 -0
- sapiopycommons/flowcyto/flowcyto_data.py +75 -0
- sapiopycommons/general/accession_service.py +375 -0
- sapiopycommons/general/aliases.py +259 -18
- sapiopycommons/general/audit_log.py +185 -0
- sapiopycommons/general/custom_report_util.py +252 -31
- sapiopycommons/general/directive_util.py +86 -0
- sapiopycommons/general/exceptions.py +69 -7
- sapiopycommons/general/popup_util.py +85 -18
- sapiopycommons/general/sapio_links.py +50 -0
- sapiopycommons/general/storage_util.py +148 -0
- sapiopycommons/general/time_util.py +97 -7
- sapiopycommons/multimodal/multimodal.py +146 -0
- sapiopycommons/multimodal/multimodal_data.py +490 -0
- sapiopycommons/processtracking/__init__.py +0 -0
- sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
- sapiopycommons/processtracking/endpoints.py +192 -0
- sapiopycommons/recordmodel/record_handler.py +653 -149
- sapiopycommons/rules/eln_rule_handler.py +89 -8
- sapiopycommons/rules/on_save_rule_handler.py +89 -12
- sapiopycommons/sftpconnect/__init__.py +0 -0
- sapiopycommons/sftpconnect/sftp_builder.py +70 -0
- sapiopycommons/webhook/webhook_context.py +39 -0
- sapiopycommons/webhook/webhook_handlers.py +617 -69
- sapiopycommons/webhook/webservice_handlers.py +317 -0
- {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/METADATA +5 -4
- sapiopycommons-2025.1.17a402.dist-info/RECORD +60 -0
- {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/WHEEL +1 -1
- sapiopycommons-2024.3.18a156.dist-info/RECORD +0 -28
- {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,375 @@
|
|
|
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,14 +1,51 @@
|
|
|
1
1
|
from collections.abc import Iterable
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Any, TypeAlias
|
|
3
3
|
|
|
4
|
+
from sapiopylib.rest.User import SapioUser
|
|
4
5
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
5
|
-
from sapiopylib.rest.
|
|
6
|
-
from sapiopylib.rest.
|
|
6
|
+
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType, AbstractVeloxFieldDefinition
|
|
7
|
+
from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
|
|
8
|
+
from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry
|
|
9
|
+
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
10
|
+
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
11
|
+
from sapiopylib.rest.utils.Protocols import ElnExperimentProtocol, ElnEntryStep
|
|
12
|
+
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel, AbstractRecordModel
|
|
13
|
+
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType, WrapperField
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
16
|
+
|
|
17
|
+
FieldValue: TypeAlias = int | float | str | bool | None
|
|
18
|
+
"""Allowable values for fields in the system."""
|
|
19
|
+
RecordModel: TypeAlias = PyRecordModel | AbstractRecordModel | WrappedRecordModel
|
|
20
|
+
"""Different forms that a record model could take."""
|
|
21
|
+
SapioRecord: TypeAlias = DataRecord | RecordModel
|
|
22
|
+
"""A record could be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel (WrappedType)."""
|
|
23
|
+
RecordIdentifier: TypeAlias = SapioRecord | int
|
|
24
|
+
"""A RecordIdentifier is either a record type or an integer for the record's record ID."""
|
|
25
|
+
DataTypeIdentifier: TypeAlias = SapioRecord | type[WrappedType] | str
|
|
26
|
+
"""A DataTypeIdentifier is either a SapioRecord, a record model wrapper type, or a string."""
|
|
27
|
+
FieldIdentifier: TypeAlias = AbstractVeloxFieldDefinition | WrapperField | str | tuple[str, FieldType]
|
|
28
|
+
"""A FieldIdentifier is either wrapper field from a record model wrapper, a string, or a tuple of string
|
|
29
|
+
and field type."""
|
|
30
|
+
FieldIdentifierKey: TypeAlias = WrapperField | str
|
|
31
|
+
"""A FieldIdentifierKey is a FieldIdentifier, except it can't be a tuple, s tuples can't be used as keys in
|
|
32
|
+
dictionaries.."""
|
|
33
|
+
HasFieldWrappers: TypeAlias = type[WrappedType] | WrappedRecordModel
|
|
34
|
+
"""An identifier for classes that have wrapper fields."""
|
|
35
|
+
ExperimentIdentifier: TypeAlias = ElnExperimentProtocol | ElnExperiment | int
|
|
36
|
+
"""An ExperimentIdentifier is either an experiment protocol, experiment, or an integer for the experiment's notebook
|
|
37
|
+
ID."""
|
|
38
|
+
ExperimentEntryIdentifier: TypeAlias = ElnEntryStep | ExperimentEntry | int
|
|
39
|
+
"""An ExperimentEntryIdentifier is either an ELN entry step, experiment entry, or an integer for the entry's ID."""
|
|
40
|
+
FieldMap: TypeAlias = dict[str, FieldValue]
|
|
41
|
+
"""A field map is simply a dict of data field names to values. The purpose of aliasing this is to help distinguish
|
|
42
|
+
any random dict in a webhook from one which is explicitly used for record fields."""
|
|
43
|
+
FieldIdentifierMap: TypeAlias = dict[FieldIdentifierKey, FieldValue]
|
|
44
|
+
"""A field identifier map is the same thing as a field map, except the keys can be field identifiers instead
|
|
45
|
+
of just strings. Note that although one of the allowed field identifiers is a tuple, you can't use tuples as
|
|
46
|
+
keys in a dictionary."""
|
|
47
|
+
UserIdentifier: TypeAlias = SapioWebhookContext | SapioUser
|
|
48
|
+
"""An identifier for classes from which a user object can be used for sending requests."""
|
|
12
49
|
|
|
13
50
|
|
|
14
51
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
@@ -16,7 +53,8 @@ class AliasUtil:
|
|
|
16
53
|
@staticmethod
|
|
17
54
|
def to_data_record(record: SapioRecord) -> DataRecord:
|
|
18
55
|
"""
|
|
19
|
-
Convert a single DataRecord, PyRecordModel, or WrappedRecordModel to just a DataRecord
|
|
56
|
+
Convert a single DataRecord, PyRecordModel, or WrappedRecordModel to just a DataRecord.
|
|
57
|
+
|
|
20
58
|
:return: The DataRecord of the input SapioRecord.
|
|
21
59
|
"""
|
|
22
60
|
return record if isinstance(record, DataRecord) else record.get_data_record()
|
|
@@ -26,30 +64,233 @@ class AliasUtil:
|
|
|
26
64
|
"""
|
|
27
65
|
Convert a list of variables that could either be DataRecords, PyRecordModels,
|
|
28
66
|
or WrappedRecordModels to just DataRecords.
|
|
67
|
+
|
|
29
68
|
:return: A list of DataRecords for the input records.
|
|
30
69
|
"""
|
|
31
70
|
return [(x if isinstance(x, DataRecord) else x.get_data_record()) for x in records]
|
|
32
71
|
|
|
33
72
|
@staticmethod
|
|
34
|
-
def to_record_ids(records: Iterable[
|
|
73
|
+
def to_record_ids(records: Iterable[RecordIdentifier]) -> list[int]:
|
|
35
74
|
"""
|
|
36
75
|
Convert a list of variables that could either be integers, DataRecords, PyRecordModels,
|
|
37
76
|
or WrappedRecordModels to just integers (taking the record ID from the records).
|
|
77
|
+
|
|
38
78
|
:return: A list of record IDs for the input records.
|
|
39
79
|
"""
|
|
40
|
-
return [(
|
|
80
|
+
return [(AliasUtil.to_record_id(x)) for x in records]
|
|
41
81
|
|
|
42
82
|
@staticmethod
|
|
43
|
-
def
|
|
83
|
+
def to_record_id(record: RecordIdentifier):
|
|
44
84
|
"""
|
|
45
|
-
Convert a
|
|
46
|
-
or
|
|
85
|
+
Convert a single variable that could be either an integer, DataRecord, PyRecordModel,
|
|
86
|
+
or WrappedRecordModel to just an integer (taking the record ID from the record).
|
|
87
|
+
|
|
88
|
+
:return: A record ID for the input record.
|
|
89
|
+
"""
|
|
90
|
+
return record if isinstance(record, int) else record.record_id
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def to_data_type_name(value: DataTypeIdentifier, convert_eln_dts: bool = True) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Convert a given value to a data type name.
|
|
96
|
+
|
|
97
|
+
:param value: A value which is a string, record, or record model type.
|
|
98
|
+
:param convert_eln_dts: If true, convert ELN data types to their base data type name.
|
|
99
|
+
:return: A string of the data type name of the input value.
|
|
100
|
+
"""
|
|
101
|
+
if isinstance(value, SapioRecord):
|
|
102
|
+
value = value.data_type_name
|
|
103
|
+
elif not isinstance(value, str):
|
|
104
|
+
value = value.get_wrapper_data_type_name()
|
|
105
|
+
if convert_eln_dts and ElnBaseDataType.is_eln_type(value):
|
|
106
|
+
return ElnBaseDataType.get_base_type(value).data_type_name
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def to_data_type_names(values: Iterable[DataTypeIdentifier], return_set: bool = False,
|
|
111
|
+
convert_eln_dts: bool = True) -> list[str] | set[str]:
|
|
112
|
+
"""
|
|
113
|
+
Convert a given iterable of values to a list or set of data type names.
|
|
114
|
+
|
|
115
|
+
:param values: An iterable of values which are strings, records, or record model types.
|
|
116
|
+
:param return_set: If true, return a set instead of a list.
|
|
117
|
+
:param convert_eln_dts: If true, convert ELN data types to their base data type name.
|
|
118
|
+
:return: A list or set of strings of the data type name of the input value.
|
|
119
|
+
"""
|
|
120
|
+
values = [AliasUtil.to_data_type_name(x, convert_eln_dts) for x in values]
|
|
121
|
+
return set(values) if return_set else values
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def to_singular_data_type_name(values: Iterable[DataTypeIdentifier], convert_eln_dts: bool = True) -> str:
|
|
125
|
+
"""
|
|
126
|
+
Convert a given iterable of values to a singular data type name that they share. Throws an exception if more
|
|
127
|
+
than one data type name exists in the provided list of identifiers.
|
|
128
|
+
|
|
129
|
+
:param values: An iterable of values which are strings, records, or record model types.
|
|
130
|
+
:param convert_eln_dts: If true, convert ELN data types to their base data type name.
|
|
131
|
+
:return: The single data type name that the input vales share. Returns an empty string if an empty iterable
|
|
132
|
+
was provided.
|
|
133
|
+
"""
|
|
134
|
+
if not values:
|
|
135
|
+
return ""
|
|
136
|
+
data_types: set[str] = AliasUtil.to_data_type_names(values, True, convert_eln_dts)
|
|
137
|
+
if len(data_types) > 1:
|
|
138
|
+
raise SapioException(f"Provided values contain multiple data types: {data_types}. "
|
|
139
|
+
f"Only expecting a single data type.")
|
|
140
|
+
return data_types.pop()
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def to_data_field_name(value: FieldIdentifier) -> str:
|
|
144
|
+
"""
|
|
145
|
+
Convert an object that can be used to identify a data field to a data field name string.
|
|
146
|
+
|
|
147
|
+
:param value: An object that can be used to identify a data field.
|
|
148
|
+
:return: A string of the data field name of the input value.
|
|
149
|
+
"""
|
|
150
|
+
if isinstance(value, tuple):
|
|
151
|
+
return value[0]
|
|
152
|
+
if isinstance(value, WrapperField):
|
|
153
|
+
return value.field_name
|
|
154
|
+
if isinstance(value, AbstractVeloxFieldDefinition):
|
|
155
|
+
return value.data_field_name
|
|
156
|
+
return value
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def to_data_field_names(values: Iterable[FieldIdentifier]) -> list[str]:
|
|
160
|
+
"""
|
|
161
|
+
Convert an iterable of objects that can be used to identify data fields to a list of data field name strings.
|
|
162
|
+
|
|
163
|
+
:param values: An iterable of objects that can be used to identify a data field.
|
|
164
|
+
:return: A list of strings of the data field names of the input values.
|
|
165
|
+
"""
|
|
166
|
+
return [AliasUtil.to_data_field_name(x) for x in values]
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def to_data_field_names_dict(values: dict[FieldIdentifierKey, Any]) -> dict[str, Any]:
|
|
170
|
+
"""
|
|
171
|
+
Take a dictionary whose keys are field identifiers and convert them all to strings for the data field name.
|
|
172
|
+
|
|
173
|
+
:param values: A dictionary of field identifiers to field values.
|
|
174
|
+
:return: A dictionary of strings of the data field names to field values for the input values.
|
|
175
|
+
"""
|
|
176
|
+
ret_dict: dict[str, FieldValue] = {}
|
|
177
|
+
for field, value in values.items():
|
|
178
|
+
ret_dict[AliasUtil.to_data_field_name(field)] = value
|
|
179
|
+
return ret_dict
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def to_data_field_names_list_dict(values: list[dict[FieldIdentifierKey, Any]]) -> list[dict[str, Any]]:
|
|
183
|
+
ret_list: list[dict[str, Any]] = []
|
|
184
|
+
for field_map in values:
|
|
185
|
+
ret_list.append(AliasUtil.to_data_field_names_dict(field_map))
|
|
186
|
+
return ret_list
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def to_field_type(field: FieldIdentifier, data_type: HasFieldWrappers | None = None) -> FieldType:
|
|
190
|
+
"""
|
|
191
|
+
Convert a given field identifier to the field type for that field.
|
|
192
|
+
|
|
193
|
+
:param field: A string or WrapperField.
|
|
194
|
+
:param data_type: If the field is provided as a string, then a record model wrapper or wrapped record model
|
|
195
|
+
must be provided to determine the field type.
|
|
196
|
+
:return: The field type of the given field.
|
|
197
|
+
"""
|
|
198
|
+
if isinstance(field, tuple):
|
|
199
|
+
return field[1]
|
|
200
|
+
if isinstance(field, WrapperField):
|
|
201
|
+
return field.field_type
|
|
202
|
+
for var in dir(data_type):
|
|
203
|
+
attr = getattr(data_type, var)
|
|
204
|
+
if isinstance(attr, WrapperField) and attr.field_name == field:
|
|
205
|
+
return attr.field_type
|
|
206
|
+
raise SapioException(f"The wrapper of data type \"{data_type.get_wrapper_data_type_name()}\" doesn't have a "
|
|
207
|
+
f"field with the name \"{field}\",")
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def to_field_map(record: SapioRecord) -> FieldMap:
|
|
211
|
+
"""
|
|
212
|
+
Convert a given record value to a field map. This includes the given RecordId of the given record.
|
|
213
|
+
|
|
214
|
+
:return: The field map for the input record.
|
|
215
|
+
"""
|
|
216
|
+
if isinstance(record, DataRecord):
|
|
217
|
+
# noinspection PyTypeChecker
|
|
218
|
+
fields: FieldMap = record.get_fields()
|
|
219
|
+
else:
|
|
220
|
+
fields: FieldMap = record.fields.copy_to_dict()
|
|
221
|
+
fields["RecordId"] = AliasUtil.to_record_id(record)
|
|
222
|
+
return fields
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def to_field_map_lists(records: Iterable[SapioRecord]) -> list[FieldMap]:
|
|
226
|
+
"""
|
|
227
|
+
Convert a list of variables that could either be DataRecords, PyRecordModels, or WrappedRecordModels
|
|
228
|
+
to a list of their field maps. This includes the given RecordId of the given records.
|
|
229
|
+
|
|
47
230
|
:return: A list of field maps for the input records.
|
|
48
231
|
"""
|
|
49
|
-
field_map_list: list[
|
|
232
|
+
field_map_list: list[FieldMap] = []
|
|
50
233
|
for record in records:
|
|
51
|
-
|
|
52
|
-
field_map_list.append(record.get_fields())
|
|
53
|
-
else:
|
|
54
|
-
field_map_list.append(record.fields.copy_to_dict())
|
|
234
|
+
field_map_list.append(AliasUtil.to_field_map(record))
|
|
55
235
|
return field_map_list
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def to_notebook_id(experiment: ExperimentIdentifier) -> int:
|
|
239
|
+
"""
|
|
240
|
+
Convert an object that identifies an ELN experiment to its notebook ID.
|
|
241
|
+
|
|
242
|
+
:return: The notebook ID for the experiment identifier.
|
|
243
|
+
"""
|
|
244
|
+
if isinstance(experiment, int):
|
|
245
|
+
return experiment
|
|
246
|
+
if isinstance(experiment, ElnExperiment):
|
|
247
|
+
return experiment.notebook_experiment_id
|
|
248
|
+
return experiment.get_id()
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def to_notebook_ids(experiments: list[ExperimentIdentifier]) -> list[int]:
|
|
252
|
+
"""
|
|
253
|
+
Convert a list of objects that identify ELN experiments to their notebook IDs.
|
|
254
|
+
|
|
255
|
+
:return: The list of notebook IDs for the experiment identifiers.
|
|
256
|
+
"""
|
|
257
|
+
notebook_ids: list[int] = []
|
|
258
|
+
for experiment in experiments:
|
|
259
|
+
notebook_ids.append(AliasUtil.to_notebook_id(experiment))
|
|
260
|
+
return notebook_ids
|
|
261
|
+
|
|
262
|
+
@staticmethod
|
|
263
|
+
def to_entry_id(entry: ExperimentEntryIdentifier) -> int:
|
|
264
|
+
"""
|
|
265
|
+
Convert an object that identifies an experiment entry to its entry ID.
|
|
266
|
+
|
|
267
|
+
:return: The entry ID for the entry identifier.
|
|
268
|
+
"""
|
|
269
|
+
if isinstance(entry, int):
|
|
270
|
+
return entry
|
|
271
|
+
elif isinstance(entry, ExperimentEntry):
|
|
272
|
+
return entry.entry_id
|
|
273
|
+
elif isinstance(entry, ElnEntryStep):
|
|
274
|
+
return entry.get_id()
|
|
275
|
+
raise SapioException(f"Unrecognized entry identifier of type {type(entry)}")
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def to_entry_ids(entries: list[ExperimentEntryIdentifier]) -> list[int]:
|
|
279
|
+
"""
|
|
280
|
+
Convert a list of objects that identify experiment entries to their entry IDs.
|
|
281
|
+
|
|
282
|
+
:return: The list of entry IDs for the entry identifiers.
|
|
283
|
+
"""
|
|
284
|
+
entry_ids: list[int] = []
|
|
285
|
+
for entry in entries:
|
|
286
|
+
entry_ids.append(AliasUtil.to_entry_id(entry))
|
|
287
|
+
return entry_ids
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def to_sapio_user(context: UserIdentifier) -> SapioUser:
|
|
291
|
+
"""
|
|
292
|
+
Convert an object that could be either a SapioUser or SapioWebhookContext to just a SapioUser.
|
|
293
|
+
|
|
294
|
+
:return: A SapioUser object.
|
|
295
|
+
"""
|
|
296
|
+
return context if isinstance(context, SapioUser) else context.user
|