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.

@@ -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: str = "Default"):
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: str, sequence_key: str, prefix: str | None = None, suffix: str | None = None,
65
- number_of_digits: int = 8, starting_value: int = 1, link_out: dict[str, str] | None = None,
66
- abstract_info: AnyFieldInfo | None = None, *, data_type_name: str | None = None,
67
- display_name: str | None = None) -> VeloxAccessionFieldDefinition:
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
- kwargs=abstract_info.__dict__)
115
+ **abstract_info.__dict__)
114
116
 
115
- def boolean_field(self, field_name: str, default_value: bool | None = False, abstract_info: AnyFieldInfo | None = None,
116
- *, data_type_name: str | None = None, display_name: str | None = None) -> VeloxBooleanFieldDefinition:
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
- kwargs=abstract_info.__dict__)
143
+ **abstract_info.__dict__)
141
144
 
142
- def date_field(self, field_name: str, default_value: int | None = None, date_time_format: str = "MMM dd, yyyy",
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: str | None = None, display_name: str | None = None) -> VeloxDateFieldDefinition:
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, kwargs=abstract_info.__dict__)
176
+ static_date, **abstract_info.__dict__)
173
177
 
174
- def date_range_field(self, field_name: str, default_value: str | DateRange | None = None,
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: str | None = None,
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, kwargs=abstract_info.__dict__)
214
+ default_value, **abstract_info.__dict__)
211
215
 
212
- def double_field(self, field_name: str, default_value: float | None = None, min_value: float = -10.**120,
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: str | None = None,
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, kwargs=abstract_info.__dict__)
246
+ precision, double_format, **abstract_info.__dict__)
243
247
 
244
- def enum_field(self, field_name: str, options: list[str], default_value: int | None = None,
245
- abstract_info: AnyFieldInfo | None = None, *, data_type_name: str | None = None,
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
- kwargs=abstract_info.__dict__)
282
+ **abstract_info.__dict__)
279
283
 
280
- def int_field(self, field_name: str, default_value: int | None = None, min_value: int = -2**31,
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: str | None = None, display_name: str | None = None) -> VeloxIntegerFieldDefinition:
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, kwargs=abstract_info.__dict__)
311
+ default_value, unique_value, **abstract_info.__dict__)
307
312
 
308
- def long_field(self, field_name: str, default_value: int | None = None, min_value: int = -2**63,
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: str | None = None, display_name: str | None = None) -> VeloxLongFieldDefinition:
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, kwargs=abstract_info.__dict__)
340
+ unique_value, **abstract_info.__dict__)
335
341
 
336
- def pick_list_field(self, field_name: str, pick_list_name: str, default_value: str | None = None,
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: str | None = None, display_name: str | None = None) -> VeloxPickListFieldDefinition:
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, kwargs=abstract_info.__dict__)
372
+ direct_edit, **abstract_info.__dict__)
366
373
 
367
- def selection_list_field(self, field_name: str, default_value: str | None = None, direct_edit: bool = False,
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: str | None = None, display_name: str | None = None) \
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
- kwargs=abstract_info.__dict__)
453
+ **abstract_info.__dict__)
447
454
 
448
- def short_field(self, field_name: str, default_value: int | None = None, min_value: int = -2**15,
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: str | None = None, display_name: str | None = None) -> VeloxShortFieldDefinition:
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, kwargs=abstract_info.__dict__)
475
-
476
- def string_field(self, field_name: str,
477
- default_value: str | None = None, max_length: int = 100, unique_value: bool = False,
478
- html_editor: bool = False, string_format: SapioStringFormat | None = None, num_lines: int = 1,
479
- auto_size: bool = False, link_out: dict[str, str] | None = None,
480
- field_validator: FieldValidator | None = None, abstract_info: AnyFieldInfo | None = None, *,
481
- data_type_name: str | None = None, display_name: str | None = None) -> VeloxStringFieldDefinition:
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, kwargs=abstract_info.__dict__)
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
- :param page_number: The page number to start the search from, If None, starts on the first page.
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.__exhaust_system_report(context, report_name, page_limit,
42
- page_size, page_number)
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.__process_results(rows, columns, filters)
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.__exhaust_custom_report(context, report_criteria, page_limit,
83
- page_size, page_number)
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.__process_results(rows, columns, filters)
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
- :param page_number: The page number to start the search from, If None, starts on the first page.
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.__exhaust_quick_report(context, report_term, page_limit,
116
- page_size, page_number)
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.__process_results(rows, columns, filters)
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, 1)
144
+ return report_man.run_system_report_by_name(report_name, 1, 0)
140
145
 
141
146
  @staticmethod
142
- def __exhaust_system_report(context: UserIdentifier,
143
- report_name: str,
144
- page_limit: int | None,
145
- page_size: int | None,
146
- page_number: int | None) \
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 __exhaust_custom_report(context: UserIdentifier,
170
- report: CustomReportCriteria,
171
- page_limit: int | None,
172
- page_size: int | None,
173
- page_number: int | None) \
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 __exhaust_quick_report(context: UserIdentifier,
201
- report_term: RawReportTerm,
202
- page_limit: int | None,
203
- page_size: int | None,
204
- page_number: int | None) \
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 __process_results(rows: list[list[FieldValue]], columns: list[ReportColumn],
228
- filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None) -> list[dict[str, FieldValue]]:
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(field_name, data_type)
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, data_type))
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, data_type)
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})
@@ -4,6 +4,7 @@ from enum import Enum
4
4
  import paramiko
5
5
  from paramiko import pkey
6
6
  from paramiko.sftp_client import SFTPClient
7
+
7
8
  from sapiopycommons.general.exceptions import SapioException
8
9
 
9
10
 
@@ -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 stace trace of the exception that was thrown is logged to 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. It a result is
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 be prepended with
225
- the user's username and the experiment ID of the experiment they are in, if any.
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 error message to the webhook server log. Log destination is stderr. This message will be prepended with
232
- the user's username and the experiment ID of the experiment they are in, if any.
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.pojo.webhook.WebhookResult import SapioWebhookResult
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
- class AbstractWebserviceHandler(AbstractWebhookHandler):
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
- A base class for constructing "webservice" endpoints on your webhook server. These are endpoints that can be
15
- communicated with by external sources without needing to format the payload JSON in the webhook context format that
16
- webhook handlers expect.
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
- Since this extends AbstractWebhookHandler, you can still register endpoints from this class in the same way you
23
- would normal webhook endpoints.
39
+ class SapioWebserviceResult:
40
+ """
41
+ A result to be returned by AbstractWebserviceHandler endpoints, for sending information back to the caller.
24
42
  """
25
- def post(self) -> dict[str, Any]:
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
- Internal method to be executed to translate incoming requests.
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
- # noinspection PyBroadException
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
- return self.run(request.json).to_json()
109
+ # noinspection PyStatementEffect
110
+ user.session_additional_data
32
111
  except Exception:
33
- print('Error occurred while running webservice custom logic. See traceback.', file=sys.stderr)
34
- traceback.print_exc()
35
- return SapioWebhookResult(False, display_text="Error occurred during webservice execution.").to_json()
36
-
37
- @abstractmethod
38
- def run(self, payload: dict[str, Any]) -> SapioWebhookResult:
39
- pass
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2024.11.21a370
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=8n0jcbMgtMUHjie4C1-IkpAuHz4zBxbZtWpr4y0kABU,36868
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=BGu9Ki0wn3m4Nk-LKM6inDSfe8ULUSG9d-HJJNOTtGc,15653
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=Uxjrq6f_cWFbqi7KRLySdOvmQGtbIBrCNyStRewqzx8,64751
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=eKYMiyBc10DNTfbeidQUcfZgFTwhu5ZU-nNJMCK_eos,3014
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=JTquLBln49L1pJ9txJ4oc4Hpzy9kYtMKs0m4SLaFx78,18363
55
- sapiopycommons/webhook/webservice_handlers.py,sha256=1J56zFI0pWl5MHoNTznvcZumITXgAHJMluj8-2BqYEw,3315
56
- sapiopycommons-2024.11.21a370.dist-info/METADATA,sha256=tzVeWwhjIQz9oWbFWvlcL1dYsD_Yl_1-DlHkhS9Wa4w,3144
57
- sapiopycommons-2024.11.21a370.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
58
- sapiopycommons-2024.11.21a370.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
59
- sapiopycommons-2024.11.21a370.dist-info/RECORD,,
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,,