sapiopycommons 2024.11.21a370__py3-none-any.whl → 2024.11.22a371__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/field_builder.py +78 -70
- sapiopycommons/general/custom_report_util.py +55 -35
- sapiopycommons/recordmodel/record_handler.py +4 -4
- sapiopycommons/sftpconnect/sftp_builder.py +1 -0
- sapiopycommons/webhook/webhook_handlers.py +8 -6
- sapiopycommons/webhook/webservice_handlers.py +342 -23
- {sapiopycommons-2024.11.21a370.dist-info → sapiopycommons-2024.11.22a371.dist-info}/METADATA +1 -1
- {sapiopycommons-2024.11.21a370.dist-info → sapiopycommons-2024.11.22a371.dist-info}/RECORD +10 -10
- {sapiopycommons-2024.11.21a370.dist-info → sapiopycommons-2024.11.22a371.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.11.21a370.dist-info → sapiopycommons-2024.11.22a371.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,6 +6,7 @@ from sapiopylib.rest.pojo.datatype.FieldDefinition import VeloxStringFieldDefini
|
|
|
6
6
|
VeloxLongFieldDefinition, VeloxPickListFieldDefinition, VeloxSelectionFieldDefinition, VeloxShortFieldDefinition, \
|
|
7
7
|
SapioDoubleFormat, ListMode
|
|
8
8
|
|
|
9
|
+
from sapiopycommons.general.aliases import FieldIdentifier, DataTypeIdentifier, AliasUtil
|
|
9
10
|
from sapiopycommons.general.exceptions import SapioException
|
|
10
11
|
|
|
11
12
|
|
|
@@ -55,16 +56,17 @@ class FieldBuilder:
|
|
|
55
56
|
"""
|
|
56
57
|
data_type: str
|
|
57
58
|
|
|
58
|
-
def __init__(self, data_type:
|
|
59
|
+
def __init__(self, data_type: DataTypeIdentifier = "Default"):
|
|
59
60
|
"""
|
|
60
61
|
:param data_type: The data type name that fields created from this builder will use as their data type.
|
|
61
62
|
"""
|
|
62
|
-
self.data_type = data_type
|
|
63
|
+
self.data_type = AliasUtil.to_data_type_name(data_type)
|
|
63
64
|
|
|
64
|
-
def accession_field(self, field_name:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
display_name: str | None = None)
|
|
65
|
+
def accession_field(self, field_name: FieldIdentifier, sequence_key: str, prefix: str | None = None,
|
|
66
|
+
suffix: str | None = None, number_of_digits: int = 8, starting_value: int = 1,
|
|
67
|
+
link_out: dict[str, str] | None = None, abstract_info: AnyFieldInfo | None = None, *,
|
|
68
|
+
data_type_name: DataTypeIdentifier | None = None, display_name: str | None = None) \
|
|
69
|
+
-> VeloxAccessionFieldDefinition:
|
|
68
70
|
"""
|
|
69
71
|
Create an accession field definition. Accession fields are text fields which generate a unique value
|
|
70
72
|
that has not been used before, incrementing from the most recently generated value. This can be used when a
|
|
@@ -98,10 +100,10 @@ class FieldBuilder:
|
|
|
98
100
|
field name doubles as the display name.
|
|
99
101
|
:return: An accession field definition with settings from the input criteria.
|
|
100
102
|
"""
|
|
103
|
+
data_type_name: str = AliasUtil.to_data_type_name(data_type_name) if data_type_name else self.data_type
|
|
104
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
101
105
|
if abstract_info is None:
|
|
102
106
|
abstract_info = AnyFieldInfo()
|
|
103
|
-
if not data_type_name:
|
|
104
|
-
data_type_name = self.data_type
|
|
105
107
|
if not display_name:
|
|
106
108
|
display_name = field_name
|
|
107
109
|
# Accession fields lock editable to false.
|
|
@@ -110,10 +112,11 @@ class FieldBuilder:
|
|
|
110
112
|
# The unique parameter has no effect, so just always set it to false.
|
|
111
113
|
return VeloxAccessionFieldDefinition(data_type_name, field_name, display_name, sequence_key, prefix, suffix,
|
|
112
114
|
number_of_digits, False, starting_value, link_out, link_out_url,
|
|
113
|
-
|
|
115
|
+
**abstract_info.__dict__)
|
|
114
116
|
|
|
115
|
-
def boolean_field(self, field_name:
|
|
116
|
-
|
|
117
|
+
def boolean_field(self, field_name: FieldIdentifier, default_value: bool | None = False,
|
|
118
|
+
abstract_info: AnyFieldInfo | None = None, *, data_type_name: DataTypeIdentifier | None = None,
|
|
119
|
+
display_name: str | None = None) -> VeloxBooleanFieldDefinition:
|
|
117
120
|
"""
|
|
118
121
|
Create a boolean field definition. Boolean fields are fields which may have a value of true or false.
|
|
119
122
|
They appear as a checkbox in the UI. Boolean fields may also have a value of null if the field is not required.
|
|
@@ -128,20 +131,21 @@ class FieldBuilder:
|
|
|
128
131
|
field name doubles as the display name.
|
|
129
132
|
:return: A boolean field definition with settings from the input criteria.
|
|
130
133
|
"""
|
|
134
|
+
data_type_name: str = AliasUtil.to_data_type_name(data_type_name) if data_type_name else self.data_type
|
|
135
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
131
136
|
if abstract_info is None:
|
|
132
137
|
abstract_info = AnyFieldInfo()
|
|
133
138
|
# Boolean fields assume that they are required if no abstract info is provided.
|
|
134
139
|
abstract_info.required = True
|
|
135
|
-
if not data_type_name:
|
|
136
|
-
data_type_name = self.data_type
|
|
137
140
|
if not display_name:
|
|
138
141
|
display_name = field_name
|
|
139
142
|
return VeloxBooleanFieldDefinition(data_type_name, field_name, display_name, default_value,
|
|
140
|
-
|
|
143
|
+
**abstract_info.__dict__)
|
|
141
144
|
|
|
142
|
-
def date_field(self, field_name:
|
|
145
|
+
def date_field(self, field_name: FieldIdentifier, default_value: int | None = None, date_time_format: str = "MMM dd, yyyy",
|
|
143
146
|
static_date: bool = False, abstract_info: AnyFieldInfo | None = None, *,
|
|
144
|
-
data_type_name:
|
|
147
|
+
data_type_name: DataTypeIdentifier | None = None, display_name: str | None = None) \
|
|
148
|
+
-> VeloxDateFieldDefinition:
|
|
145
149
|
"""
|
|
146
150
|
Create a date field definition. Date fields store date and time information as an integer
|
|
147
151
|
representing the number of milliseconds since the unix epoch. This timestamp is then displayed to users in a
|
|
@@ -162,18 +166,18 @@ class FieldBuilder:
|
|
|
162
166
|
field name doubles as the display name.
|
|
163
167
|
:return: A date field definition with settings from the input criteria.
|
|
164
168
|
"""
|
|
169
|
+
data_type_name: str = AliasUtil.to_data_type_name(data_type_name) if data_type_name else self.data_type
|
|
170
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
165
171
|
if abstract_info is None:
|
|
166
172
|
abstract_info = AnyFieldInfo()
|
|
167
|
-
if not data_type_name:
|
|
168
|
-
data_type_name = self.data_type
|
|
169
173
|
if not display_name:
|
|
170
174
|
display_name = field_name
|
|
171
175
|
return VeloxDateFieldDefinition(data_type_name, field_name, display_name, date_time_format, default_value,
|
|
172
|
-
static_date,
|
|
176
|
+
static_date, **abstract_info.__dict__)
|
|
173
177
|
|
|
174
|
-
def date_range_field(self, field_name:
|
|
178
|
+
def date_range_field(self, field_name: FieldIdentifier, default_value: str | DateRange | None = None,
|
|
175
179
|
date_time_format: str = "MMM dd, yyyy", static_date: bool = False,
|
|
176
|
-
abstract_info: AnyFieldInfo | None = None, *, data_type_name:
|
|
180
|
+
abstract_info: AnyFieldInfo | None = None, *, data_type_name: DataTypeIdentifier | None = None,
|
|
177
181
|
display_name: str | None = None) -> VeloxDateRangeFieldDefinition:
|
|
178
182
|
"""
|
|
179
183
|
Create a date range field definition. Date range fields store two unix epoch timestamps as a string of the
|
|
@@ -198,20 +202,20 @@ class FieldBuilder:
|
|
|
198
202
|
field name doubles as the display name.
|
|
199
203
|
:return: A date range field definition with settings from the input criteria.
|
|
200
204
|
"""
|
|
205
|
+
data_type_name: str = AliasUtil.to_data_type_name(data_type_name) if data_type_name else self.data_type
|
|
206
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
201
207
|
if abstract_info is None:
|
|
202
208
|
abstract_info = AnyFieldInfo()
|
|
203
|
-
if not data_type_name:
|
|
204
|
-
data_type_name = self.data_type
|
|
205
209
|
if not display_name:
|
|
206
210
|
display_name = field_name
|
|
207
211
|
if isinstance(default_value, DateRange):
|
|
208
212
|
default_value = str(default_value)
|
|
209
213
|
return VeloxDateRangeFieldDefinition(data_type_name, field_name, display_name, date_time_format, static_date,
|
|
210
|
-
default_value,
|
|
214
|
+
default_value, **abstract_info.__dict__)
|
|
211
215
|
|
|
212
|
-
def double_field(self, field_name:
|
|
216
|
+
def double_field(self, field_name: FieldIdentifier, default_value: float | None = None, min_value: float = -10.**120,
|
|
213
217
|
max_value: float = 10.**120, precision: int = 1, double_format: SapioDoubleFormat | None = None,
|
|
214
|
-
abstract_info: AnyFieldInfo | None = None, *, data_type_name:
|
|
218
|
+
abstract_info: AnyFieldInfo | None = None, *, data_type_name: DataTypeIdentifier | None = None,
|
|
215
219
|
display_name: str | None = None) -> VeloxDoubleFieldDefinition:
|
|
216
220
|
"""
|
|
217
221
|
Create a double field definition. Double fields represent decimal numerical values. They can also
|
|
@@ -232,17 +236,17 @@ class FieldBuilder:
|
|
|
232
236
|
field name doubles as the display name.
|
|
233
237
|
:return: A double field definition with settings from the input criteria.
|
|
234
238
|
"""
|
|
239
|
+
data_type_name: str = AliasUtil.to_data_type_name(data_type_name) if data_type_name else self.data_type
|
|
240
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
235
241
|
if abstract_info is None:
|
|
236
242
|
abstract_info = AnyFieldInfo()
|
|
237
|
-
if not data_type_name:
|
|
238
|
-
data_type_name = self.data_type
|
|
239
243
|
if not display_name:
|
|
240
244
|
display_name = field_name
|
|
241
245
|
return VeloxDoubleFieldDefinition(data_type_name, field_name, display_name, min_value, max_value, default_value,
|
|
242
|
-
precision, double_format,
|
|
246
|
+
precision, double_format, **abstract_info.__dict__)
|
|
243
247
|
|
|
244
|
-
def enum_field(self, field_name:
|
|
245
|
-
abstract_info: AnyFieldInfo | None = None, *, data_type_name:
|
|
248
|
+
def enum_field(self, field_name: FieldIdentifier, options: list[str], default_value: int | None = None,
|
|
249
|
+
abstract_info: AnyFieldInfo | None = None, *, data_type_name: DataTypeIdentifier | None = None,
|
|
246
250
|
display_name: str | None = None) -> VeloxEnumFieldDefinition:
|
|
247
251
|
"""
|
|
248
252
|
Create an enum field definition. Enum fields allow for the display of a list of options as a field
|
|
@@ -268,18 +272,19 @@ class FieldBuilder:
|
|
|
268
272
|
field name doubles as the display name.
|
|
269
273
|
:return: An enum field definition with settings from the input criteria.
|
|
270
274
|
"""
|
|
275
|
+
data_type_name: str = AliasUtil.to_data_type_name(data_type_name) if data_type_name else self.data_type
|
|
276
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
271
277
|
if abstract_info is None:
|
|
272
278
|
abstract_info = AnyFieldInfo()
|
|
273
|
-
if not data_type_name:
|
|
274
|
-
data_type_name = self.data_type
|
|
275
279
|
if not display_name:
|
|
276
280
|
display_name = field_name
|
|
277
281
|
return VeloxEnumFieldDefinition(data_type_name, field_name, display_name, default_value, options,
|
|
278
|
-
|
|
282
|
+
**abstract_info.__dict__)
|
|
279
283
|
|
|
280
|
-
def int_field(self, field_name:
|
|
284
|
+
def int_field(self, field_name: FieldIdentifier, default_value: int | None = None, min_value: int = -2**31,
|
|
281
285
|
max_value: int = 2**31 - 1, unique_value: bool = False, abstract_info: AnyFieldInfo | None = None, *,
|
|
282
|
-
data_type_name:
|
|
286
|
+
data_type_name: DataTypeIdentifier | None = None, display_name: str | None = None) \
|
|
287
|
+
-> VeloxIntegerFieldDefinition:
|
|
283
288
|
"""
|
|
284
289
|
Create an integer field definition. Integer fields are 32-bit whole numbers.
|
|
285
290
|
|
|
@@ -296,18 +301,19 @@ class FieldBuilder:
|
|
|
296
301
|
field name doubles as the display name.
|
|
297
302
|
:return: An integer field definition with settings from the input criteria.
|
|
298
303
|
"""
|
|
304
|
+
data_type_name: str = AliasUtil.to_data_type_name(data_type_name) if data_type_name else self.data_type
|
|
305
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
299
306
|
if abstract_info is None:
|
|
300
307
|
abstract_info = AnyFieldInfo()
|
|
301
|
-
if not data_type_name:
|
|
302
|
-
data_type_name = self.data_type
|
|
303
308
|
if not display_name:
|
|
304
309
|
display_name = field_name
|
|
305
310
|
return VeloxIntegerFieldDefinition(data_type_name, field_name, display_name, min_value, max_value,
|
|
306
|
-
default_value, unique_value,
|
|
311
|
+
default_value, unique_value, **abstract_info.__dict__)
|
|
307
312
|
|
|
308
|
-
def long_field(self, field_name:
|
|
313
|
+
def long_field(self, field_name: FieldIdentifier, default_value: int | None = None, min_value: int = -2**63,
|
|
309
314
|
max_value: int = 2**63 - 1, unique_value: bool = False, abstract_info: AnyFieldInfo | None = None, *,
|
|
310
|
-
data_type_name:
|
|
315
|
+
data_type_name: DataTypeIdentifier | None = None, display_name: str | None = None) \
|
|
316
|
+
-> VeloxLongFieldDefinition:
|
|
311
317
|
"""
|
|
312
318
|
Create a long field definition. Long fields are 64-bit whole numbers.
|
|
313
319
|
|
|
@@ -324,18 +330,19 @@ class FieldBuilder:
|
|
|
324
330
|
field name doubles as the display name.
|
|
325
331
|
:return: A long field definition with settings from the input criteria.
|
|
326
332
|
"""
|
|
333
|
+
data_type_name: str = AliasUtil.to_data_type_name(data_type_name) if data_type_name else self.data_type
|
|
334
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
327
335
|
if abstract_info is None:
|
|
328
336
|
abstract_info = AnyFieldInfo()
|
|
329
|
-
if not data_type_name:
|
|
330
|
-
data_type_name = self.data_type
|
|
331
337
|
if not display_name:
|
|
332
338
|
display_name = field_name
|
|
333
339
|
return VeloxLongFieldDefinition(data_type_name, field_name, display_name, min_value, max_value, default_value,
|
|
334
|
-
unique_value,
|
|
340
|
+
unique_value, **abstract_info.__dict__)
|
|
335
341
|
|
|
336
|
-
def pick_list_field(self, field_name:
|
|
342
|
+
def pick_list_field(self, field_name: FieldIdentifier, pick_list_name: str, default_value: str | None = None,
|
|
337
343
|
direct_edit: bool = False, abstract_info: AnyFieldInfo | None = None, *,
|
|
338
|
-
data_type_name:
|
|
344
|
+
data_type_name: DataTypeIdentifier | None = None, display_name: str | None = None) \
|
|
345
|
+
-> VeloxPickListFieldDefinition:
|
|
339
346
|
"""
|
|
340
347
|
Create a pick list field definition. Pick list fields are string fields that display a drop-down list of options
|
|
341
348
|
when being edited by a user. The list of options is backed by a pick list defined in the list manager sections
|
|
@@ -355,22 +362,22 @@ class FieldBuilder:
|
|
|
355
362
|
field name doubles as the display name.
|
|
356
363
|
:return: A pick list field definition with settings from the input criteria.
|
|
357
364
|
"""
|
|
365
|
+
data_type_name: str = AliasUtil.to_data_type_name(data_type_name) if data_type_name else self.data_type
|
|
366
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
358
367
|
if abstract_info is None:
|
|
359
368
|
abstract_info = AnyFieldInfo()
|
|
360
|
-
if not data_type_name:
|
|
361
|
-
data_type_name = self.data_type
|
|
362
369
|
if not display_name:
|
|
363
370
|
display_name = field_name
|
|
364
371
|
return VeloxPickListFieldDefinition(data_type_name, field_name, display_name, pick_list_name, default_value,
|
|
365
|
-
direct_edit,
|
|
372
|
+
direct_edit, **abstract_info.__dict__)
|
|
366
373
|
|
|
367
|
-
def selection_list_field(self, field_name:
|
|
368
|
-
multi_select: bool = False, unique_value: bool = False,
|
|
374
|
+
def selection_list_field(self, field_name: FieldIdentifier, default_value: str | None = None,
|
|
375
|
+
direct_edit: bool = False, multi_select: bool = False, unique_value: bool = False,
|
|
369
376
|
abstract_info: AnyFieldInfo | None = None, *, pick_list_name: str | None = None,
|
|
370
377
|
custom_report_name: str | None = None, plugin_name: str | None = None,
|
|
371
378
|
static_values: list[str] | None = None, user_list: bool = False,
|
|
372
379
|
user_group_list: bool = False, non_api_user_list: bool = False,
|
|
373
|
-
data_type_name:
|
|
380
|
+
data_type_name: DataTypeIdentifier | None = None, display_name: str | None = None) \
|
|
374
381
|
-> VeloxSelectionFieldDefinition:
|
|
375
382
|
"""
|
|
376
383
|
Create a selection list field definition. Selection list fields are string fields that display a drop-down list
|
|
@@ -401,10 +408,10 @@ class FieldBuilder:
|
|
|
401
408
|
field name doubles as the display name.
|
|
402
409
|
:return: A selection list field definition with settings from the input criteria.
|
|
403
410
|
"""
|
|
411
|
+
data_type_name: str = AliasUtil.to_data_type_name(data_type_name) if data_type_name else self.data_type
|
|
412
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
404
413
|
if abstract_info is None:
|
|
405
414
|
abstract_info = AnyFieldInfo()
|
|
406
|
-
if not data_type_name:
|
|
407
|
-
data_type_name = self.data_type
|
|
408
415
|
if not display_name:
|
|
409
416
|
display_name = field_name
|
|
410
417
|
|
|
@@ -443,11 +450,12 @@ class FieldBuilder:
|
|
|
443
450
|
list_mode, unique_value, multi_select,
|
|
444
451
|
default_value, pick_list_name, custom_report_name,
|
|
445
452
|
plugin_name, direct_edit, static_values,
|
|
446
|
-
|
|
453
|
+
**abstract_info.__dict__)
|
|
447
454
|
|
|
448
|
-
def short_field(self, field_name:
|
|
455
|
+
def short_field(self, field_name: FieldIdentifier, default_value: int | None = None, min_value: int = -2**15,
|
|
449
456
|
max_value: int = 2**15 - 1, unique_value: bool = False, abstract_info: AnyFieldInfo | None = None,
|
|
450
|
-
*, data_type_name:
|
|
457
|
+
*, data_type_name: DataTypeIdentifier | None = None, display_name: str | None = None) \
|
|
458
|
+
-> VeloxShortFieldDefinition:
|
|
451
459
|
"""
|
|
452
460
|
Create a short field definition. Short fields are 16-bit whole numbers.
|
|
453
461
|
|
|
@@ -464,21 +472,21 @@ class FieldBuilder:
|
|
|
464
472
|
field name doubles as the display name.
|
|
465
473
|
:return: A short field definition with settings from the input criteria.
|
|
466
474
|
"""
|
|
475
|
+
data_type_name: str = AliasUtil.to_data_type_name(data_type_name) if data_type_name else self.data_type
|
|
476
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
467
477
|
if abstract_info is None:
|
|
468
478
|
abstract_info = AnyFieldInfo()
|
|
469
|
-
if not data_type_name:
|
|
470
|
-
data_type_name = self.data_type
|
|
471
479
|
if not display_name:
|
|
472
480
|
display_name = field_name
|
|
473
481
|
return VeloxShortFieldDefinition(data_type_name, field_name, display_name, min_value, max_value, default_value,
|
|
474
|
-
unique_value,
|
|
475
|
-
|
|
476
|
-
def string_field(self, field_name: str,
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
+
unique_value, **abstract_info.__dict__)
|
|
483
|
+
|
|
484
|
+
def string_field(self, field_name: FieldIdentifier, default_value: str | None = None, max_length: int = 100,
|
|
485
|
+
unique_value: bool = False, html_editor: bool = False,
|
|
486
|
+
string_format: SapioStringFormat | None = None, num_lines: int = 1, auto_size: bool = False,
|
|
487
|
+
link_out: dict[str, str] | None = None, field_validator: FieldValidator | None = None,
|
|
488
|
+
abstract_info: AnyFieldInfo | None = None, *, data_type_name: DataTypeIdentifier | None = None,
|
|
489
|
+
display_name: str | None = None) -> VeloxStringFieldDefinition:
|
|
482
490
|
"""
|
|
483
491
|
Create a string field definition. String fields represent text, and are highly customizable, allowing the
|
|
484
492
|
field to be plain text or rich HTML, take up one line of space or multiple on a form, format as emails or
|
|
@@ -516,16 +524,16 @@ class FieldBuilder:
|
|
|
516
524
|
field name doubles as the display name.
|
|
517
525
|
:return: A string field definition with settings from the input criteria.
|
|
518
526
|
"""
|
|
527
|
+
data_type_name: str = AliasUtil.to_data_type_name(data_type_name) if data_type_name else self.data_type
|
|
528
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
519
529
|
if abstract_info is None:
|
|
520
530
|
abstract_info = AnyFieldInfo()
|
|
521
|
-
if not data_type_name:
|
|
522
|
-
data_type_name = self.data_type
|
|
523
531
|
if not display_name:
|
|
524
532
|
display_name = field_name
|
|
525
533
|
link_out, link_out_url = self._convert_link_out(link_out)
|
|
526
534
|
return VeloxStringFieldDefinition(data_type_name, field_name, display_name, default_value, max_length,
|
|
527
535
|
unique_value, html_editor, string_format, num_lines, auto_size, link_out,
|
|
528
|
-
link_out_url, field_validator,
|
|
536
|
+
link_out_url, field_validator, **abstract_info.__dict__)
|
|
529
537
|
|
|
530
538
|
@staticmethod
|
|
531
539
|
def _convert_link_out(link_out: dict[str, str] | None) -> tuple[bool, str | None]:
|
|
@@ -29,8 +29,10 @@ class CustomReportUtil:
|
|
|
29
29
|
filter on. Only those headers that both the filters and the custom report share will take effect. That is,
|
|
30
30
|
any filters that have a header name that isn't in the custom report will be ignored.
|
|
31
31
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
32
|
-
:param page_size: The size of each page of results in the search. If None, the page size is set by the server
|
|
33
|
-
|
|
32
|
+
:param page_size: The size of each page of results in the search. If None, the page size is set by the server,
|
|
33
|
+
which may be unlimited.
|
|
34
|
+
:param page_number: The page number to start the search from, If None, starts on the first page. Note that the
|
|
35
|
+
number of the first page is 0.
|
|
34
36
|
:return: The results of the report listed row by row, mapping each cell to the header it is under. The header
|
|
35
37
|
values in the dicts are the data field names of the columns.
|
|
36
38
|
If two columns in the search have the same data field name but differing data type names, then the
|
|
@@ -38,11 +40,11 @@ class CustomReportUtil:
|
|
|
38
40
|
had a Sample column with a data field name of Identifier and a Request column with the same data field name,
|
|
39
41
|
then the dictionary keys for these columns would be Sample.Identifier and Request.Identifier respectively.
|
|
40
42
|
"""
|
|
41
|
-
results: tuple = CustomReportUtil.
|
|
42
|
-
|
|
43
|
+
results: tuple = CustomReportUtil._exhaust_system_report(context, report_name, page_limit,
|
|
44
|
+
page_size, page_number)
|
|
43
45
|
columns: list[ReportColumn] = results[0]
|
|
44
46
|
rows: list[list[FieldValue]] = results[1]
|
|
45
|
-
return CustomReportUtil.
|
|
47
|
+
return CustomReportUtil._process_results(rows, columns, filters)
|
|
46
48
|
|
|
47
49
|
@staticmethod
|
|
48
50
|
def run_custom_report(context: UserIdentifier,
|
|
@@ -71,7 +73,8 @@ class CustomReportUtil:
|
|
|
71
73
|
:param page_size: The size of each page of results in the search. If None, uses the value from the given report
|
|
72
74
|
criteria. If not None, overwrites the value from the given report criteria.
|
|
73
75
|
:param page_number: The page number to start the search from, If None, uses the value from the given report
|
|
74
|
-
criteria. If not None, overwrites the value from the given report criteria.
|
|
76
|
+
criteria. If not None, overwrites the value from the given report criteria. Note that the number of the
|
|
77
|
+
first page is 0.
|
|
75
78
|
:return: The results of the report listed row by row, mapping each cell to the header it is under. The header
|
|
76
79
|
values in the dicts are the data field names of the columns.
|
|
77
80
|
If two columns in the search have the same data field name but differing data type names, then the
|
|
@@ -79,11 +82,11 @@ class CustomReportUtil:
|
|
|
79
82
|
had a Sample column with a data field name of Identifier and a Request column with the same data field name,
|
|
80
83
|
then the dictionary keys for these columns would be Sample.Identifier and Request.Identifier respectively.
|
|
81
84
|
"""
|
|
82
|
-
results: tuple = CustomReportUtil.
|
|
83
|
-
|
|
85
|
+
results: tuple = CustomReportUtil._exhaust_custom_report(context, report_criteria, page_limit,
|
|
86
|
+
page_size, page_number)
|
|
84
87
|
columns: list[ReportColumn] = results[0]
|
|
85
88
|
rows: list[list[FieldValue]] = results[1]
|
|
86
|
-
return CustomReportUtil.
|
|
89
|
+
return CustomReportUtil._process_results(rows, columns, filters)
|
|
87
90
|
|
|
88
91
|
@staticmethod
|
|
89
92
|
def run_quick_report(context: UserIdentifier,
|
|
@@ -107,16 +110,18 @@ class CustomReportUtil:
|
|
|
107
110
|
filter on. Only those headers that both the filters and the custom report share will take effect. That is,
|
|
108
111
|
any filters that have a header name that isn't in the custom report will be ignored.
|
|
109
112
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
110
|
-
:param page_size: The size of each page of results in the search. If None, the page size is set by the server
|
|
111
|
-
|
|
113
|
+
:param page_size: The size of each page of results in the search. If None, the page size is set by the server,
|
|
114
|
+
which may be unlimited.
|
|
115
|
+
:param page_number: The page number to start the search from, If None, starts on the first page. Note that the
|
|
116
|
+
number of the first page is 0.
|
|
112
117
|
:return: The results of the report listed row by row, mapping each cell to the header it is under. The header
|
|
113
118
|
values in the dicts are the data field names of the columns.
|
|
114
119
|
"""
|
|
115
|
-
results: tuple = CustomReportUtil.
|
|
116
|
-
|
|
120
|
+
results: tuple = CustomReportUtil._exhaust_quick_report(context, report_term, page_limit,
|
|
121
|
+
page_size, page_number)
|
|
117
122
|
columns: list[ReportColumn] = results[0]
|
|
118
123
|
rows: list[list[FieldValue]] = results[1]
|
|
119
|
-
return CustomReportUtil.
|
|
124
|
+
return CustomReportUtil._process_results(rows, columns, filters)
|
|
120
125
|
|
|
121
126
|
@staticmethod
|
|
122
127
|
def get_system_report_criteria(context: UserIdentifier, report_name: str) -> CustomReport:
|
|
@@ -136,14 +141,14 @@ class CustomReportUtil:
|
|
|
136
141
|
"""
|
|
137
142
|
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
138
143
|
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
139
|
-
return report_man.run_system_report_by_name(report_name, 1,
|
|
144
|
+
return report_man.run_system_report_by_name(report_name, 1, 0)
|
|
140
145
|
|
|
141
146
|
@staticmethod
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
def _exhaust_system_report(context: UserIdentifier,
|
|
148
|
+
report_name: str,
|
|
149
|
+
page_limit: int | None,
|
|
150
|
+
page_size: int | None,
|
|
151
|
+
page_number: int | None) \
|
|
147
152
|
-> tuple[list[ReportColumn], list[list[FieldValue]]]:
|
|
148
153
|
"""
|
|
149
154
|
Given a system report, iterate over every page of the report and collect the results
|
|
@@ -152,6 +157,11 @@ class CustomReportUtil:
|
|
|
152
157
|
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
153
158
|
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
154
159
|
|
|
160
|
+
# If a page size was provided but no page number was provided, then set the page number to 0,
|
|
161
|
+
# as both parameters are necessary in order to get paged results.
|
|
162
|
+
if page_size is not None and page_number is None:
|
|
163
|
+
page_number = 0
|
|
164
|
+
|
|
155
165
|
result = None
|
|
156
166
|
has_next_page: bool = True
|
|
157
167
|
rows: list[list[FieldValue]] = []
|
|
@@ -159,18 +169,18 @@ class CustomReportUtil:
|
|
|
159
169
|
while has_next_page and (not page_limit or cur_page <= page_limit):
|
|
160
170
|
result = report_man.run_system_report_by_name(report_name, page_size, page_number)
|
|
161
171
|
page_size = result.page_size
|
|
162
|
-
page_number = result.page_number
|
|
172
|
+
page_number = result.page_number + 1
|
|
163
173
|
has_next_page = result.has_next_page
|
|
164
174
|
rows.extend(result.result_table)
|
|
165
175
|
cur_page += 1
|
|
166
176
|
return result.column_list, rows
|
|
167
177
|
|
|
168
178
|
@staticmethod
|
|
169
|
-
def
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
def _exhaust_custom_report(context: UserIdentifier,
|
|
180
|
+
report: CustomReportCriteria,
|
|
181
|
+
page_limit: int | None,
|
|
182
|
+
page_size: int | None,
|
|
183
|
+
page_number: int | None) \
|
|
174
184
|
-> tuple[list[ReportColumn], list[list[FieldValue]]]:
|
|
175
185
|
"""
|
|
176
186
|
Given a custom report, iterate over every page of the report and collect the results
|
|
@@ -179,6 +189,11 @@ class CustomReportUtil:
|
|
|
179
189
|
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
180
190
|
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
181
191
|
|
|
192
|
+
# If a page size was provided but no page number was provided, then set the page number to 0,
|
|
193
|
+
# as both parameters are necessary in order to get paged results.
|
|
194
|
+
if page_size is not None and page_number is None:
|
|
195
|
+
page_number = 0
|
|
196
|
+
|
|
182
197
|
result = None
|
|
183
198
|
if page_size is not None:
|
|
184
199
|
report.page_size = page_size
|
|
@@ -190,18 +205,18 @@ class CustomReportUtil:
|
|
|
190
205
|
while has_next_page and (not page_limit or cur_page <= page_limit):
|
|
191
206
|
result = report_man.run_custom_report(report)
|
|
192
207
|
report.page_size = result.page_size
|
|
193
|
-
report.page_number = result.page_number
|
|
208
|
+
report.page_number = result.page_number + 1
|
|
194
209
|
has_next_page = result.has_next_page
|
|
195
210
|
rows.extend(result.result_table)
|
|
196
211
|
cur_page += 1
|
|
197
212
|
return result.column_list, rows
|
|
198
213
|
|
|
199
214
|
@staticmethod
|
|
200
|
-
def
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
215
|
+
def _exhaust_quick_report(context: UserIdentifier,
|
|
216
|
+
report_term: RawReportTerm,
|
|
217
|
+
page_limit: int | None,
|
|
218
|
+
page_size: int | None,
|
|
219
|
+
page_number: int | None) \
|
|
205
220
|
-> tuple[list[ReportColumn], list[list[FieldValue]]]:
|
|
206
221
|
"""
|
|
207
222
|
Given a quick report, iterate over every page of the report and collect the results
|
|
@@ -210,6 +225,11 @@ class CustomReportUtil:
|
|
|
210
225
|
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
211
226
|
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
212
227
|
|
|
228
|
+
# If a page size was provided but no page number was provided, then set the page number to 0,
|
|
229
|
+
# as both parameters are necessary in order to get paged results.
|
|
230
|
+
if page_size is not None and page_number is None:
|
|
231
|
+
page_number = 0
|
|
232
|
+
|
|
213
233
|
result = None
|
|
214
234
|
has_next_page: bool = True
|
|
215
235
|
rows: list[list[FieldValue]] = []
|
|
@@ -217,15 +237,15 @@ class CustomReportUtil:
|
|
|
217
237
|
while has_next_page and (not page_limit or cur_page <= page_limit):
|
|
218
238
|
result = report_man.run_quick_report(report_term, page_size, page_number)
|
|
219
239
|
page_size = result.page_size
|
|
220
|
-
page_number = result.page_number
|
|
240
|
+
page_number = result.page_number + 1
|
|
221
241
|
has_next_page = result.has_next_page
|
|
222
242
|
rows.extend(result.result_table)
|
|
223
243
|
cur_page += 1
|
|
224
244
|
return result.column_list, rows
|
|
225
245
|
|
|
226
246
|
@staticmethod
|
|
227
|
-
def
|
|
228
|
-
|
|
247
|
+
def _process_results(rows: list[list[FieldValue]], columns: list[ReportColumn],
|
|
248
|
+
filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None) -> list[dict[str, FieldValue]]:
|
|
229
249
|
"""
|
|
230
250
|
Given the results of a report as a list of row values and the report's columns, combine these lists to
|
|
231
251
|
result in a singular list of dictionaries for each row in the results.
|
|
@@ -297,7 +297,7 @@ class RecordHandler:
|
|
|
297
297
|
not None, in which case it overwrites the given report's value.
|
|
298
298
|
:param page_number: The page number to start the search from, If None, starts on the first page.
|
|
299
299
|
If the input report is a custom report criteria, uses the value from the criteria, unless this value is
|
|
300
|
-
not None, in which case it overwrites the given report's value.
|
|
300
|
+
not None, in which case it overwrites the given report's value. Note that the number of the first page is 0.
|
|
301
301
|
:return: The record models for the queried records that matched the given report.
|
|
302
302
|
"""
|
|
303
303
|
if isinstance(report_name, str):
|
|
@@ -966,7 +966,7 @@ class RecordHandler:
|
|
|
966
966
|
current = current.get_forward_side_link(node.data_field_name)
|
|
967
967
|
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
968
968
|
field_name: str = node.data_field_name
|
|
969
|
-
reverse_links: list[PyRecordModel] = current.get_reverse_side_link(
|
|
969
|
+
reverse_links: list[PyRecordModel] = current.get_reverse_side_link(data_type, field_name)
|
|
970
970
|
if not reverse_links:
|
|
971
971
|
current = None
|
|
972
972
|
elif len(reverse_links) > 1:
|
|
@@ -1016,7 +1016,7 @@ class RecordHandler:
|
|
|
1016
1016
|
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
1017
1017
|
next_search.add(search.get_forward_side_link(node.data_field_name))
|
|
1018
1018
|
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
1019
|
-
next_search.update(search.get_reverse_side_link(node.data_field_name
|
|
1019
|
+
next_search.update(search.get_reverse_side_link(data_type, node.data_field_name))
|
|
1020
1020
|
else:
|
|
1021
1021
|
raise SapioException("Unsupported path direction.")
|
|
1022
1022
|
current_search = next_search
|
|
@@ -1064,7 +1064,7 @@ class RecordHandler:
|
|
|
1064
1064
|
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
1065
1065
|
current = [current[0].get_forward_side_link(node.data_field_name)]
|
|
1066
1066
|
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
1067
|
-
current = current[0].get_reverse_side_link(node.data_field_name
|
|
1067
|
+
current = current[0].get_reverse_side_link(data_type, node.data_field_name)
|
|
1068
1068
|
else:
|
|
1069
1069
|
raise SapioException("Unsupported path direction.")
|
|
1070
1070
|
ret_dict.update({model: self.inst_man.wrap(current[0], wrapper_type) if current else None})
|
|
@@ -190,7 +190,7 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
190
190
|
Handle a generic exception which isn't one of the handled Sapio exceptions.
|
|
191
191
|
|
|
192
192
|
Default behavior returns a false webhook result with a generic error message as display text informing the user
|
|
193
|
-
to contact Sapio support. Additionally, the
|
|
193
|
+
to contact Sapio support. Additionally, the stack trace of the exception that was thrown is logged to the
|
|
194
194
|
execution log for the webhook call in the system.
|
|
195
195
|
|
|
196
196
|
:param e: The exception that was raised.
|
|
@@ -214,22 +214,24 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
214
214
|
|
|
215
215
|
:param e: The exception that was raised.
|
|
216
216
|
:return: An optional SapioWebhookResult. May return a custom message to the client that wouldn't have been
|
|
217
|
-
sent by one of the normal exception handlers, or may return None if no result needs returned.
|
|
217
|
+
sent by one of the normal exception handlers, or may return None if no result needs returned. If a result is
|
|
218
218
|
returned, then the default behavior of other exception handlers is skipped.
|
|
219
219
|
"""
|
|
220
220
|
return None
|
|
221
221
|
|
|
222
222
|
def log_info(self, msg: str) -> None:
|
|
223
223
|
"""
|
|
224
|
-
Write an info message to the webhook server log. Log destination is stdout. This message will
|
|
225
|
-
the user's
|
|
224
|
+
Write an info message to the webhook server log. Log destination is stdout. This message will include
|
|
225
|
+
information about the user's group, their location in the system, the webhook invocation type, and other
|
|
226
|
+
important information that can be gathered from the context that is useful for debugging.
|
|
226
227
|
"""
|
|
227
228
|
self.logger.info(self._format_log(msg, "log_info call"))
|
|
228
229
|
|
|
229
230
|
def log_error(self, msg: str) -> None:
|
|
230
231
|
"""
|
|
231
|
-
Write an
|
|
232
|
-
the user's
|
|
232
|
+
Write an info message to the webhook server log. Log destination is stdout. This message will include
|
|
233
|
+
information about the user's group, their location in the system, the webhook invocation type, and other
|
|
234
|
+
important information that can be gathered from the context that is useful for debugging.
|
|
233
235
|
"""
|
|
234
236
|
# PR-46209: Use logger.error instead of logger.info when logging errors.
|
|
235
237
|
self.logger.error(self._format_log(msg, "log_error call"))
|
|
@@ -1,42 +1,119 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import traceback
|
|
3
|
-
from abc import abstractmethod
|
|
3
|
+
from abc import abstractmethod, ABC
|
|
4
|
+
from base64 import b64decode
|
|
5
|
+
from logging import Logger
|
|
4
6
|
from typing import Any
|
|
5
7
|
|
|
6
|
-
from flask import request
|
|
8
|
+
from flask import request, Response, Request
|
|
9
|
+
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
7
10
|
from sapiopylib.rest.User import SapioUser
|
|
8
11
|
from sapiopylib.rest.WebhookService import AbstractWebhookHandler
|
|
9
|
-
from sapiopylib.rest.
|
|
12
|
+
from sapiopylib.rest.utils.DataTypeCacheManager import DataTypeCacheManager
|
|
13
|
+
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager, \
|
|
14
|
+
RecordModelRelationshipManager
|
|
15
|
+
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
16
|
+
from werkzeug.datastructures import Headers
|
|
17
|
+
from werkzeug.datastructures.structures import MultiDict
|
|
10
18
|
|
|
19
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
20
|
+
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
11
21
|
|
|
12
|
-
|
|
22
|
+
|
|
23
|
+
class SapioWebserviceException(Exception):
|
|
24
|
+
"""
|
|
25
|
+
An exception to be thrown by webservice classes when responding to a request with an error.
|
|
13
26
|
"""
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
msg: str
|
|
28
|
+
code: int
|
|
29
|
+
|
|
30
|
+
def __init__(self, msg: str, code: int = 500):
|
|
31
|
+
"""
|
|
32
|
+
:param msg: The message to return in the webservice response.
|
|
33
|
+
:param code: The status code to return in the webservice response.
|
|
34
|
+
"""
|
|
35
|
+
self.msg = msg
|
|
36
|
+
self.code = code
|
|
17
37
|
|
|
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
38
|
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
class SapioWebserviceResult:
|
|
40
|
+
"""
|
|
41
|
+
A result to be returned by AbstractWebserviceHandler endpoints, for sending information back to the caller.
|
|
24
42
|
"""
|
|
25
|
-
|
|
43
|
+
message: str
|
|
44
|
+
is_error: bool
|
|
45
|
+
status_code: int
|
|
46
|
+
payload: dict[str, Any]
|
|
47
|
+
|
|
48
|
+
def __init__(self, message: str = "Success", status_code: int = 200, is_error: bool = False,
|
|
49
|
+
payload: dict[str, Any] | None = None):
|
|
26
50
|
"""
|
|
27
|
-
|
|
51
|
+
:param message: A message to return to the sender describing what happened.
|
|
52
|
+
:param status_code: An HTTP status code to return to the sender.
|
|
53
|
+
:param is_error: Whether the webservice had an error during processing.
|
|
54
|
+
:param payload: A payload of additional information to return to the sender.
|
|
28
55
|
"""
|
|
29
|
-
|
|
56
|
+
self.message = message
|
|
57
|
+
self.status_code = status_code
|
|
58
|
+
self.is_error = is_error
|
|
59
|
+
self.payload = payload
|
|
60
|
+
if payload is None:
|
|
61
|
+
self.payload = {}
|
|
62
|
+
|
|
63
|
+
def to_json(self) -> dict[str, Any]:
|
|
64
|
+
return {"message": self.message,
|
|
65
|
+
"statusCode": self.status_code,
|
|
66
|
+
"isError": self.is_error,
|
|
67
|
+
"payload": self.payload}
|
|
68
|
+
|
|
69
|
+
def to_result(self) -> tuple[dict[str, Any], int]:
|
|
70
|
+
return self.to_json(), self.status_code
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
WebserviceResponse = SapioWebserviceResult | Response | tuple[dict[str, Any] | int]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class BaseAuthenticatorClass(AbstractWebhookHandler, ABC):
|
|
77
|
+
"""
|
|
78
|
+
The base class for classes that may need to authenticate a SapioUser object for interacting with a Sapio server
|
|
79
|
+
through the webservice API.
|
|
80
|
+
"""
|
|
81
|
+
def authenticate_user(self, headers: dict[str, str]) -> SapioUser:
|
|
82
|
+
"""
|
|
83
|
+
Authenticate a user for making requests to a Sapio server using the provided headers. If no user can be
|
|
84
|
+
authenticated, then an exception will be thrown.
|
|
85
|
+
|
|
86
|
+
:param headers: The headers of the webservice request.
|
|
87
|
+
:return: A SapioUser object used to make requests to a Sapio server as authorized by the headers.
|
|
88
|
+
"""
|
|
89
|
+
# Get the system URL from the headers.
|
|
90
|
+
if "System-URL" not in headers:
|
|
91
|
+
raise SapioWebserviceException("No \"System-URL\" provided in headers.", 400)
|
|
92
|
+
url: str = headers.get("System-URL")
|
|
93
|
+
if not url.endswith("/webservice/api"):
|
|
94
|
+
raise SapioWebserviceException(f"\"System-URL\" must be a webservice API URL for the target system: {url}", 400)
|
|
95
|
+
|
|
96
|
+
# Get the login credentials from the headers.
|
|
97
|
+
auth: str = headers.get("Authorization")
|
|
98
|
+
if auth and auth.startswith("Basic "):
|
|
99
|
+
credentials: list[str] = b64decode(auth.split("Basic ")[1]).decode().split(":")
|
|
100
|
+
user = self.basic_auth(url, credentials[0], credentials[1])
|
|
101
|
+
elif auth and auth.startswith("Bearer "):
|
|
102
|
+
user = self.bearer_token_auth(url, auth.split("Bearer ")[1])
|
|
103
|
+
elif "X-API-TOKEN" in headers:
|
|
104
|
+
user = self.api_token_auth(url, headers.get("X-API-TOKEN"))
|
|
105
|
+
else:
|
|
106
|
+
raise SapioWebserviceException(f"Unrecognized Authorization method.", 400)
|
|
107
|
+
# Make a simple webservice call to confirm that the credentials are valid.
|
|
30
108
|
try:
|
|
31
|
-
|
|
109
|
+
# noinspection PyStatementEffect
|
|
110
|
+
user.session_additional_data
|
|
32
111
|
except Exception:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def run(self, payload: dict[str, Any]) -> SapioWebhookResult:
|
|
39
|
-
pass
|
|
112
|
+
if "Unauthorized (javax.ws.rs.NotAuthorizedException: Incorrect username or password.)" in traceback.format_exc():
|
|
113
|
+
raise SapioWebserviceException("Unauthorized. Incorrect username or password.", 401)
|
|
114
|
+
else:
|
|
115
|
+
raise SapioWebserviceException("System-URL is invalid or user cannot be authenticated.", 401)
|
|
116
|
+
return user
|
|
40
117
|
|
|
41
118
|
def basic_auth(self, url: str, username: str, password: str) -> SapioUser:
|
|
42
119
|
"""
|
|
@@ -65,3 +142,245 @@ class AbstractWebserviceHandler(AbstractWebhookHandler):
|
|
|
65
142
|
:return: A SapioUser that will authenticate requests using a bearer token.
|
|
66
143
|
"""
|
|
67
144
|
return SapioUser(url, self.verify_sapio_cert, self.client_timeout_seconds, bearer_token=bearer_token)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class AbstractWebservicePostHandler(BaseAuthenticatorClass):
|
|
148
|
+
"""
|
|
149
|
+
A base class for constructing POST webservice endpoints on your webhook server. These are endpoints that can be
|
|
150
|
+
communicated with by external sources without needing to format the request in the webhook context format that
|
|
151
|
+
normal webhook handlers expect.
|
|
152
|
+
|
|
153
|
+
Since this extends AbstractWebhookHandler, you can still register endpoints from this class in the same way you
|
|
154
|
+
would normal webhook endpoints.
|
|
155
|
+
"""
|
|
156
|
+
request: Request
|
|
157
|
+
|
|
158
|
+
def post(self) -> Response | tuple[dict[str, Any], int]:
|
|
159
|
+
"""
|
|
160
|
+
Internal method to be executed to translate incoming POST requests.
|
|
161
|
+
"""
|
|
162
|
+
# noinspection PyBroadException
|
|
163
|
+
try:
|
|
164
|
+
self.request = request
|
|
165
|
+
ret_val: WebserviceResponse = self.run(request.json, request.headers, request.args)
|
|
166
|
+
if isinstance(ret_val, SapioWebserviceResult):
|
|
167
|
+
return ret_val.to_result()
|
|
168
|
+
return ret_val
|
|
169
|
+
except Exception:
|
|
170
|
+
print('Error occurred while running webservice custom logic. See traceback.', file=sys.stderr)
|
|
171
|
+
traceback.print_exc()
|
|
172
|
+
return SapioWebserviceResult("Unexpected error occurred.", 500, True).to_json(), 500
|
|
173
|
+
|
|
174
|
+
# noinspection PyMethodOverriding
|
|
175
|
+
@abstractmethod
|
|
176
|
+
def run(self, payload: Any, headers: Headers, params: MultiDict[str, str]) -> WebserviceResponse:
|
|
177
|
+
"""
|
|
178
|
+
The execution details for this endpoint.
|
|
179
|
+
|
|
180
|
+
:param payload: The JSON payload from the request. Usually a dictionary of strings to Any.
|
|
181
|
+
:param headers: The headers from the request. Can be used like a dictionary.
|
|
182
|
+
:param params: The URL parameters from the request.
|
|
183
|
+
:return: A response object to send back to the requester.
|
|
184
|
+
"""
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class AbstractWebserviceGetHandler(BaseAuthenticatorClass):
|
|
189
|
+
"""
|
|
190
|
+
A base class for constructing GET webservice endpoints on your webhook server. These are endpoints that can be
|
|
191
|
+
communicated with by external sources without needing to format the request in the webhook context format that
|
|
192
|
+
normal webhook handlers expect.
|
|
193
|
+
|
|
194
|
+
Since this extends AbstractWebhookHandler, you can still register endpoints from this class in the same way you
|
|
195
|
+
would normal webhook endpoints.
|
|
196
|
+
"""
|
|
197
|
+
request: Request
|
|
198
|
+
|
|
199
|
+
def get(self) -> Response | tuple[dict[str, Any], int]:
|
|
200
|
+
"""
|
|
201
|
+
Internal method to be executed to translate incoming GET requests.
|
|
202
|
+
"""
|
|
203
|
+
# noinspection PyBroadException
|
|
204
|
+
try:
|
|
205
|
+
self.request = request
|
|
206
|
+
ret_val: WebserviceResponse = self.run(request.headers, request.args)
|
|
207
|
+
if isinstance(ret_val, SapioWebserviceResult):
|
|
208
|
+
return ret_val.to_result()
|
|
209
|
+
return ret_val
|
|
210
|
+
except Exception:
|
|
211
|
+
print('Error occurred while running webservice custom logic. See traceback.', file=sys.stderr)
|
|
212
|
+
traceback.print_exc()
|
|
213
|
+
return SapioWebserviceResult("Unexpected error occurred.", 500, True).to_json(), 500
|
|
214
|
+
|
|
215
|
+
# noinspection PyMethodOverriding
|
|
216
|
+
@abstractmethod
|
|
217
|
+
def run(self, headers: Headers, params: MultiDict[str, str]) -> WebserviceResponse:
|
|
218
|
+
"""
|
|
219
|
+
The execution details for this endpoint.
|
|
220
|
+
|
|
221
|
+
:param headers: The headers from the request. Can be used like a dictionary.
|
|
222
|
+
:param params: The URL parameters from the request.
|
|
223
|
+
:return: A response object to send back to the requester.
|
|
224
|
+
"""
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class CommonsWebserviceBaseHandler(BaseAuthenticatorClass, ABC):
|
|
229
|
+
"""
|
|
230
|
+
A base class for all commons webservice handlers.
|
|
231
|
+
"""
|
|
232
|
+
logger: Logger
|
|
233
|
+
|
|
234
|
+
user: SapioUser | None
|
|
235
|
+
|
|
236
|
+
dr_man: DataRecordManager
|
|
237
|
+
rec_man: RecordModelManager
|
|
238
|
+
inst_man: RecordModelInstanceManager
|
|
239
|
+
rel_man: RecordModelRelationshipManager
|
|
240
|
+
an_man: RecordModelAncestorManager
|
|
241
|
+
|
|
242
|
+
dt_cache: DataTypeCacheManager
|
|
243
|
+
rec_handler: RecordHandler
|
|
244
|
+
|
|
245
|
+
def initialize(self, headers: Headers) -> None:
|
|
246
|
+
"""
|
|
247
|
+
A function that can be optionally overridden by your classes to initialize additional instance variables,
|
|
248
|
+
or set up whatever else you wish to set up before the execute function is ran. Default behavior initializes a
|
|
249
|
+
SapioUser and various manager classes to make requests from.
|
|
250
|
+
"""
|
|
251
|
+
self.user = None
|
|
252
|
+
self.user = self.authenticate_user(headers)
|
|
253
|
+
|
|
254
|
+
self.logger = self.user.logger
|
|
255
|
+
|
|
256
|
+
self.dr_man = DataRecordManager(self.user)
|
|
257
|
+
self.rec_man = RecordModelManager(self.user)
|
|
258
|
+
self.inst_man = self.rec_man.instance_manager
|
|
259
|
+
self.rel_man = self.rec_man.relationship_manager
|
|
260
|
+
self.an_man = RecordModelAncestorManager(self.rec_man)
|
|
261
|
+
|
|
262
|
+
self.dt_cache = DataTypeCacheManager(self.user)
|
|
263
|
+
self.rec_handler = RecordHandler(self.user)
|
|
264
|
+
|
|
265
|
+
def handle_webservice_exception(self, e: SapioWebserviceException) -> WebserviceResponse:
|
|
266
|
+
"""
|
|
267
|
+
Handle a generic exception which isn't one of the handled Sapio exceptions.
|
|
268
|
+
|
|
269
|
+
Default behavior returns a webservice result with the message and error code of the webservice exception.
|
|
270
|
+
Additionally, the stack trace of the exception that was thrown is logged to the webhook server.
|
|
271
|
+
|
|
272
|
+
:param e: The exception that was raised.
|
|
273
|
+
:return: A webservice result to return to the requester.
|
|
274
|
+
"""
|
|
275
|
+
result: WebserviceResponse | None = self.handle_any_exception(e)
|
|
276
|
+
if result is not None:
|
|
277
|
+
return result
|
|
278
|
+
msg: str = traceback.format_exc()
|
|
279
|
+
self.log_error(msg)
|
|
280
|
+
return SapioWebserviceResult(e.msg, e.code, True)
|
|
281
|
+
|
|
282
|
+
def handle_unexpected_exception(self, e: Exception) -> WebserviceResponse:
|
|
283
|
+
"""
|
|
284
|
+
Handle a generic exception which isn't one of the handled Sapio exceptions.
|
|
285
|
+
|
|
286
|
+
Default behavior returns a 500 code result with a generic error message informing the user to contact Sapio
|
|
287
|
+
support. Additionally, the stack trace of the exception that was thrown is logged to the webhook server.
|
|
288
|
+
|
|
289
|
+
:param e: The exception that was raised.
|
|
290
|
+
:return: A webservice result to return to the requester.
|
|
291
|
+
"""
|
|
292
|
+
result: SapioWebserviceResult | None = self.handle_any_exception(e)
|
|
293
|
+
if result is not None:
|
|
294
|
+
return result
|
|
295
|
+
msg: str = traceback.format_exc()
|
|
296
|
+
self.log_error(msg)
|
|
297
|
+
return SapioWebserviceResult("Unexpected error occurred during webservice execution. "
|
|
298
|
+
"Please contact Sapio support.", 500, True)
|
|
299
|
+
|
|
300
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
301
|
+
def handle_any_exception(self, e: Exception) -> WebserviceResponse | None:
|
|
302
|
+
"""
|
|
303
|
+
An exception handler which runs regardless of the type of exception that was raised. Can be used to "rollback"
|
|
304
|
+
the client if an error occurs. Default behavior does nothing and returns None.
|
|
305
|
+
|
|
306
|
+
:param e: The exception that was raised.
|
|
307
|
+
:return: An optional response to the caller. May return a custom message to the client that wouldn't have been
|
|
308
|
+
sent by one of the normal exception handlers, or may return None if no result needs returned. If a result is
|
|
309
|
+
returned, then the default behavior of other exception handlers is skipped.
|
|
310
|
+
"""
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
def log_info(self, msg: str) -> None:
|
|
314
|
+
"""
|
|
315
|
+
Write an info message to the webhook server log. Log destination is stdout.
|
|
316
|
+
"""
|
|
317
|
+
self.logger.info(msg)
|
|
318
|
+
|
|
319
|
+
def log_error(self, msg: str) -> None:
|
|
320
|
+
"""
|
|
321
|
+
Write an error message to the webhook server log. Log destination is stderr.
|
|
322
|
+
"""
|
|
323
|
+
self.logger.error(msg)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class CommonsWebservicePostHandler(AbstractWebservicePostHandler, CommonsWebserviceBaseHandler):
|
|
327
|
+
"""
|
|
328
|
+
A subclass of AbstractWebservicePostHandler that provides additional quality of life features, including
|
|
329
|
+
authentication of a SapioUser from the request headers, initialization of various commonly used managers, and more.
|
|
330
|
+
"""
|
|
331
|
+
def run(self, payload: Any, headers: Headers, params: MultiDict[str, str]) -> WebserviceResponse:
|
|
332
|
+
try:
|
|
333
|
+
self.initialize(headers)
|
|
334
|
+
result = self.execute(self.user, payload, headers, params)
|
|
335
|
+
if result is None:
|
|
336
|
+
raise SapioException("Your execute function returned a None result! Don't forget your return statement!")
|
|
337
|
+
return result
|
|
338
|
+
except SapioWebserviceException as e:
|
|
339
|
+
return self.handle_webservice_exception(e)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
return self.handle_unexpected_exception(e)
|
|
342
|
+
|
|
343
|
+
@abstractmethod
|
|
344
|
+
def execute(self, user: SapioUser, payload: Any, headers: Headers, params: MultiDict[str, str]) \
|
|
345
|
+
-> SapioWebserviceResult:
|
|
346
|
+
"""
|
|
347
|
+
The execution details for this endpoint.
|
|
348
|
+
|
|
349
|
+
:param user: The SapioUser object authenticated from the request headers.
|
|
350
|
+
:param payload: The JSON payload from the request. Usually a dictionary of strings to Any.
|
|
351
|
+
:param headers: The headers from the request. Can be used like a dictionary.
|
|
352
|
+
:param params: The URL parameters from the request.
|
|
353
|
+
:return: A response object to send back to the requester.
|
|
354
|
+
"""
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class CommonsWebserviceGetHandler(AbstractWebserviceGetHandler, CommonsWebserviceBaseHandler):
|
|
359
|
+
"""
|
|
360
|
+
A subclass of AbstractWebserviceGetHandler that provides additional quality of life features, including
|
|
361
|
+
authentication of a SapioUser from the request headers, initialization of various commonly used managers, and more.
|
|
362
|
+
"""
|
|
363
|
+
def run(self, headers: Headers, params: MultiDict[str, str]) -> WebserviceResponse:
|
|
364
|
+
try:
|
|
365
|
+
self.initialize(headers)
|
|
366
|
+
result = self.execute(self.user, headers, params)
|
|
367
|
+
if result is None:
|
|
368
|
+
raise SapioException("Your execute function returned a None result! Don't forget your return statement!")
|
|
369
|
+
return result
|
|
370
|
+
except SapioWebserviceException as e:
|
|
371
|
+
return self.handle_webservice_exception(e)
|
|
372
|
+
except Exception as e:
|
|
373
|
+
return self.handle_unexpected_exception(e)
|
|
374
|
+
|
|
375
|
+
@abstractmethod
|
|
376
|
+
def execute(self, user: SapioUser, headers: Headers, params: MultiDict[str, str]) \
|
|
377
|
+
-> SapioWebserviceResult:
|
|
378
|
+
"""
|
|
379
|
+
The execution details for this endpoint.
|
|
380
|
+
|
|
381
|
+
:param user: The SapioUser object authenticated from the request headers.
|
|
382
|
+
:param headers: The headers from the request. Can be used like a dictionary.
|
|
383
|
+
:param params: The URL parameters from the request.
|
|
384
|
+
:return: A response object to send back to the requester.
|
|
385
|
+
"""
|
|
386
|
+
pass
|
{sapiopycommons-2024.11.21a370.dist-info → sapiopycommons-2024.11.22a371.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sapiopycommons
|
|
3
|
-
Version: 2024.11.
|
|
3
|
+
Version: 2024.11.22a371
|
|
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>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
sapiopycommons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
sapiopycommons/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
sapiopycommons/callbacks/callback_util.py,sha256=nb6cXK8yFq96gtG0Z2NiK-qdNaRh88bavUH-ZoBjh18,67953
|
|
4
|
-
sapiopycommons/callbacks/field_builder.py,sha256=
|
|
4
|
+
sapiopycommons/callbacks/field_builder.py,sha256=p2XacN99MuKk3ite8GAqstUMpixqugul2CsC4gB83-o,38620
|
|
5
5
|
sapiopycommons/chem/IndigoMolecules.py,sha256=3f-aig3AJkKJhRmhlQ0cI-5G8oeaQk_3foJTDZCvoko,2040
|
|
6
6
|
sapiopycommons/chem/Molecules.py,sha256=SQKnqdZnhYj_6HGtEZmE_1DormonRR1-nBAQ__z4gms,11485
|
|
7
7
|
sapiopycommons/chem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -31,7 +31,7 @@ sapiopycommons/general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
|
31
31
|
sapiopycommons/general/accession_service.py,sha256=HYgyOsH_UaoRnoury-c2yTW8SeG4OtjLemdpCzoV4R8,13484
|
|
32
32
|
sapiopycommons/general/aliases.py,sha256=tdDBNWSGx6s39eHJ3n2kscc4xxW3ZYaUfDftct6FmJE,12910
|
|
33
33
|
sapiopycommons/general/audit_log.py,sha256=KQI0AGgN9WLwKqnHE4Tm0xeBCfpVBf8rIQ2HFmnyFGI,8956
|
|
34
|
-
sapiopycommons/general/custom_report_util.py,sha256=
|
|
34
|
+
sapiopycommons/general/custom_report_util.py,sha256=6ZIg_Jl02TYUygfc5xqBoI1srPsSxLyxaJ9jwTolGcM,16671
|
|
35
35
|
sapiopycommons/general/exceptions.py,sha256=GY7fe0qOgoy4kQVn_Pn3tdzHsJZyNIpa6VCChg6tzuM,1813
|
|
36
36
|
sapiopycommons/general/popup_util.py,sha256=L-4qpTemSZdlD6_6oEsDYIzLOCiZgDK6wC6DqUwzOYA,31925
|
|
37
37
|
sapiopycommons/general/sapio_links.py,sha256=o9Z-8y2rz6AI0Cy6tq58ElPge9RBnisGc9NyccbaJxs,2610
|
|
@@ -43,17 +43,17 @@ sapiopycommons/processtracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
|
|
|
43
43
|
sapiopycommons/processtracking/custom_workflow_handler.py,sha256=0Si5RQ1YFmqmcZWV8jNDKTffix2iZnQJ6b97fn31pbc,23859
|
|
44
44
|
sapiopycommons/processtracking/endpoints.py,sha256=w5bziI2xC7450M95rCF8JpRwkoni1kEDibyAux9B12Q,10848
|
|
45
45
|
sapiopycommons/recordmodel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
-
sapiopycommons/recordmodel/record_handler.py,sha256=
|
|
46
|
+
sapiopycommons/recordmodel/record_handler.py,sha256=5kCO5zsSg0kp3uYpgw1vf0WLHw30pMNC_6Bn3G7iQkI,64796
|
|
47
47
|
sapiopycommons/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
48
48
|
sapiopycommons/rules/eln_rule_handler.py,sha256=JYzDA_14D2nLnlqwbpIxVOrfKWzbOS27AYf4TQfGr4Q,10469
|
|
49
49
|
sapiopycommons/rules/on_save_rule_handler.py,sha256=Rkqvph20RbNq6m-RF4fbvCP-YfD2CZYBM2iTr3nl0eY,10236
|
|
50
50
|
sapiopycommons/sftpconnect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
|
-
sapiopycommons/sftpconnect/sftp_builder.py,sha256=
|
|
51
|
+
sapiopycommons/sftpconnect/sftp_builder.py,sha256=lFK3FeXk-sFLefW0hqY8WGUQDeYiGaT6yDACzT_zFgQ,3015
|
|
52
52
|
sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
53
|
sapiopycommons/webhook/webhook_context.py,sha256=D793uLsb1691SalaPnBUk3rOSxn_hYLhdvkaIxjNXss,1909
|
|
54
|
-
sapiopycommons/webhook/webhook_handlers.py,sha256=
|
|
55
|
-
sapiopycommons/webhook/webservice_handlers.py,sha256=
|
|
56
|
-
sapiopycommons-2024.11.
|
|
57
|
-
sapiopycommons-2024.11.
|
|
58
|
-
sapiopycommons-2024.11.
|
|
59
|
-
sapiopycommons-2024.11.
|
|
54
|
+
sapiopycommons/webhook/webhook_handlers.py,sha256=MdsVK4bHffkMNmNWl0_qvu-5Lz8-qGu4Ryi7lZO1BZs,18586
|
|
55
|
+
sapiopycommons/webhook/webservice_handlers.py,sha256=AFM2Va9Fpb2BvlFycIUUOdghtGiEv1Ab5jf44yjHSgU,17218
|
|
56
|
+
sapiopycommons-2024.11.22a371.dist-info/METADATA,sha256=IZ6j1XeF-tLgm-0ds4XSLeNoPzi-_F1wTLEOabg6NmM,3144
|
|
57
|
+
sapiopycommons-2024.11.22a371.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
58
|
+
sapiopycommons-2024.11.22a371.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
|
|
59
|
+
sapiopycommons-2024.11.22a371.dist-info/RECORD,,
|
|
File without changes
|
{sapiopycommons-2024.11.21a370.dist-info → sapiopycommons-2024.11.22a371.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|