sapiopycommons 2024.8.28a313__py3-none-any.whl → 2024.8.28a314__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.

@@ -3,8 +3,10 @@ from typing import Any
3
3
 
4
4
  from sapiopylib.rest.DataRecordManagerService import DataRecordManager
5
5
  from sapiopylib.rest.User import SapioUser
6
+ from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, RawReportTerm, ReportColumn
6
7
  from sapiopylib.rest.pojo.DataRecord import DataRecord
7
8
  from sapiopylib.rest.pojo.DataRecordPaging import DataRecordPojoPageCriteria
9
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
8
10
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
9
11
  from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
10
12
  QueryAllRecordsOfTypeAutoPager
@@ -75,6 +77,43 @@ class RecordHandler:
75
77
  """
76
78
  return self.query_models_with_criteria(wrapper_type, field, value_list, None, page_limit)[0]
77
79
 
80
+ def query_and_map_models(self, wrapper_type: type[WrappedType], field: str, value_list: Iterable[Any],
81
+ page_limit: int | None = None, *, mapping_field: str | None = None) \
82
+ -> dict[Any, list[WrappedType]]:
83
+ """
84
+ Shorthand for using query_models to search for records given values on a specific field and then using
85
+ map_by_field to turn the returned list into a dictionary mapping field values to records.
86
+
87
+ :param wrapper_type: The record model wrapper to use.
88
+ :param field: The field to query and map on.
89
+ :param value_list: The values of the field to query on.
90
+ :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
91
+ :param mapping_field: If provided, use this field to map against instead of the field that was queried on.
92
+ :return: The record models for the queried records mapped by field values to the records with that value.
93
+ """
94
+ if mapping_field is None:
95
+ mapping_field = field
96
+ return self.map_by_field(self.query_models(wrapper_type, field, value_list, page_limit), mapping_field)
97
+
98
+ def query_and_unique_map_models(self, wrapper_type: type[WrappedType], field: str, value_list: Iterable[Any],
99
+ page_limit: int | None = None, *, mapping_field: str | None = None) \
100
+ -> dict[Any, WrappedType]:
101
+ """
102
+ Shorthand for using query_models to search for records given values on a specific field and then using
103
+ map_by_unique_field to turn the returned list into a dictionary mapping field values to records.
104
+ If any two records share the same field value, throws an exception.
105
+
106
+ :param wrapper_type: The record model wrapper to use.
107
+ :param field: The field to query and map on.
108
+ :param value_list: The values of the field to query on.
109
+ :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
110
+ :param mapping_field: If provided, use this field to map against instead of the field that was queried on.
111
+ :return: The record models for the queried records mapped by field values to the record with that value.
112
+ """
113
+ if mapping_field is None:
114
+ mapping_field = field
115
+ return self.map_by_unique_field(self.query_models(wrapper_type, field, value_list, page_limit), mapping_field)
116
+
78
117
  def query_models_with_criteria(self, wrapper_type: type[WrappedType], field: str, value_list: Iterable[Any],
79
118
  paging_criteria: DataRecordPojoPageCriteria | None = None,
80
119
  page_limit: int | None = None) \
@@ -160,24 +199,58 @@ class RecordHandler:
160
199
  return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
161
200
 
162
201
  def query_models_by_report(self, wrapper_type: type[WrappedType],
163
- report_name: str,
202
+ report_name: str | RawReportTerm | CustomReportCriteria,
164
203
  filters: dict[str, Iterable[Any]] | None = None,
165
- page_limit: int | None = None) -> list[WrappedType]:
204
+ page_limit: int | None = None,
205
+ page_size: int | None = None,
206
+ page_number: int | None = None) -> list[WrappedType]:
166
207
  """
167
- Run a system report that contains a RecordId column and query for the records with those IDs.
168
- First runs the custom report, then runs a data record manager query on the results of the custom report.
208
+ Run a report and use the results of that report to query for and return the records in the report results.
209
+ First runs the report, then runs a data record manager query on the results of the custom report.
210
+
211
+ Will throw an exception if given the name of a system report that does not have a RecordId column.
212
+ Quick and custom reports are guaranteed to have a record ID column.
169
213
 
170
- Will throw an exception if the given system report does not have a RecordId column.
214
+ Any given custom report criteria should only have columns from a single data type.
171
215
 
172
216
  :param wrapper_type: The record model wrapper to use.
173
- :param report_name: The name of the system report to run.
217
+ :param report_name: The name of a system report, or a raw report term for a quick report, or custom report
218
+ criteria for a custom report.
174
219
  :param filters: If provided, filter the results of the report using the given mapping of headers to values to
175
220
  filter on. This filtering is done before the records are queried.
176
221
  :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
177
- :return: The record models for the queried records.
178
- """
179
- results: list[dict[str, Any]] = CustomReportUtil.run_system_report(self.user, report_name, filters, page_limit)
180
- # Using the bracket operators because we want to throw an exception if RecordId doesn't exist in the report.
222
+ :param page_size: The size of each page of results in the search. If None, the page size is set by the server.
223
+ If the input report is a custom report criteria, uses the value from the criteria, unless this value is
224
+ not None, in which case it overwrites the given report's value.
225
+ :param page_number: The page number to start the search from, If None, starts on the first page.
226
+ If the input report is a custom report criteria, uses the value from the criteria, unless this value is
227
+ not None, in which case it overwrites the given report's value.
228
+ :return: The record models for the queried records that matched the given report.
229
+ """
230
+ if isinstance(report_name, str):
231
+ results: list[dict[str, Any]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
232
+ page_limit, page_size, page_number)
233
+ elif isinstance(report_name, RawReportTerm):
234
+ results: list[dict[str, Any]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
235
+ page_limit, page_size, page_number)
236
+ elif isinstance(report_name, CustomReportCriteria):
237
+ dt: str = wrapper_type.get_wrapper_data_type_name()
238
+ # Ensure that the root data type is the one we're looking for.
239
+ report_name.root_data_type = dt
240
+ # Raise an exception if any column in the report doesn't match the given data type.
241
+ if any([x.data_type_name != dt for x in report_name.column_list]):
242
+ raise SapioException("You may only query records from a report containing columns from that data type.")
243
+ # Enforce that the given custom report has a record ID column.
244
+ if not any([x.data_type_name == dt and x.data_field_name == "RecordId" for x in report_name.column_list]):
245
+ report_name.column_list.append(ReportColumn(dt, "RecordId", FieldType.LONG))
246
+ results: list[dict[str, Any]] = CustomReportUtil.run_custom_report(self.user, report_name, filters,
247
+ page_limit, page_size, page_number)
248
+ else:
249
+ raise SapioException("Unrecognized report object.")
250
+
251
+ # Using the bracket accessor because we want to throw an exception if RecordId doesn't exist in the report.
252
+ # This should only possibly be the case with system reports, as quick reports will include the record ID and
253
+ # we forced any given custom report to have a record ID column.
181
254
  ids: list[int] = [row["RecordId"] for row in results]
182
255
  return self.query_models_by_id(wrapper_type, ids)
183
256
 
@@ -211,10 +284,46 @@ class RecordHandler:
211
284
  the fields in the fields list.
212
285
  """
213
286
  models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
214
- for model, field in zip(models, fields):
215
- model.set_field_values(field)
287
+ for model, field_list in zip(models, fields):
288
+ model.set_field_values(field_list)
216
289
  return models
217
290
 
291
+ def find_or_add_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value: Any,
292
+ secondary_identifiers: FieldMap | None = None) -> WrappedType:
293
+ """
294
+ Find a unique record that matches the given field values. If no such records exist, add a record model to the
295
+ cache with the identifying fields set to the desired values. This record will be created in the system when
296
+ you store and commit changes. If more than one record with the identifying values exists, throws an exception.
297
+
298
+ The record is searched for using the primary identifier field name and value. If multiple records are returned
299
+ by the query on this primary identifier, then the secondary identifiers are used to filter the results.
300
+
301
+ Makes a webservice call to query for the existing record.
302
+
303
+ :param wrapper_type: The record model wrapper to use.
304
+ :param primary_identifier: The data field name of the field to search on.
305
+ :param id_value: The value of the identifying field to search for.
306
+ :param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
307
+ the primary identifier.
308
+ :return: The record model with the identifying field value, either pulled from the system or newly created.
309
+ """
310
+ # PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
311
+ # If no secondary identifiers were provided, use an empty dictionary.
312
+ if secondary_identifiers is None:
313
+ secondary_identifiers = {}
314
+
315
+ unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
316
+ secondary_identifiers)
317
+ # If a unique record matched the identifiers, return it.
318
+ if unique_record is not None:
319
+ return unique_record
320
+
321
+ # If none of the results matched the identifiers, create a new record with all identifiers set.
322
+ # Put the primary identifier and value into the secondary identifiers list and use that as the fields map
323
+ # for this new record.
324
+ secondary_identifiers.update({primary_identifier: id_value})
325
+ return self.add_models_with_data(wrapper_type, [secondary_identifiers])[0]
326
+
218
327
  def create_models(self, wrapper_type: type[WrappedType], num: int) -> list[WrappedType]:
219
328
  """
220
329
  Shorthand for creating new records via the data record manager and then returning them as wrapped
@@ -270,24 +379,8 @@ class RecordHandler:
270
379
  if secondary_identifiers is None:
271
380
  secondary_identifiers = {}
272
381
 
273
- # Query for all records that match the primary identifier.
274
- results: list[WrappedType] = self.query_models(wrapper_type, primary_identifier, [id_value])
275
-
276
- # Find the one record, if any, that matches the secondary identifiers.
277
- unique_record: WrappedType | None = None
278
- for result in results:
279
- matches_all: bool = True
280
- for field, value in secondary_identifiers.items():
281
- if result.get_field_value(field) != value:
282
- matches_all = False
283
- break
284
- if matches_all:
285
- # If a previous record in the results already matched all identifiers, then throw an exception.
286
- if unique_record is not None:
287
- raise SapioException(f"More than one record of type {wrapper_type.get_wrapper_data_type_name()} "
288
- f"encountered in system that matches all provided identifiers.")
289
- unique_record = result
290
-
382
+ unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
383
+ secondary_identifiers)
291
384
  # If a unique record matched the identifiers, return it.
292
385
  if unique_record is not None:
293
386
  return unique_record
@@ -329,6 +422,29 @@ class RecordHandler:
329
422
  return_dict[model] = model.get_parents_of_type(parent_type)
330
423
  return return_dict
331
424
 
425
+ @staticmethod
426
+ def map_by_parent(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
427
+ -> dict[WrappedType, RecordModel]:
428
+ """
429
+ Take a list of record models and map them by their parent. Essentially an inversion of map_to_parent.
430
+ If two records share the same parent, an exception will be thrown. The parents must already be loaded.
431
+
432
+ :param models: A list of record models.
433
+ :param parent_type: The record model wrapper of the parents.
434
+ :return: A dict[ParentType, ModelType]. If an input model doesn't have a parent of the given parent type,
435
+ then it will not be in the resulting dictionary.
436
+ """
437
+ to_parent: dict[RecordModel, WrappedType] = RecordHandler.map_to_parent(models, parent_type)
438
+ by_parent: dict[WrappedType, RecordModel] = {}
439
+ for record, parent in to_parent.items():
440
+ if parent is None:
441
+ continue
442
+ if parent in by_parent:
443
+ raise SapioException(f"Parent {parent.data_type_name} {parent.record_id} encountered more than once "
444
+ f"in models list.")
445
+ by_parent[parent] = record
446
+ return by_parent
447
+
332
448
  @staticmethod
333
449
  def map_by_parents(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
334
450
  -> dict[WrappedType, list[RecordModel]]:
@@ -379,6 +495,29 @@ class RecordHandler:
379
495
  return_dict[model] = model.get_children_of_type(child_type)
380
496
  return return_dict
381
497
 
498
+ @staticmethod
499
+ def map_by_child(models: Iterable[RecordModel], child_type: type[WrappedType]) \
500
+ -> dict[WrappedType, list[RecordModel]]:
501
+ """
502
+ Take a list of record models and map them by their children. Essentially an inversion of map_to_child.
503
+ If two records share the same child, an exception will be thrown. The children must already be loaded.
504
+
505
+ :param models: A list of record models.
506
+ :param child_type: The record model wrapper of the children.
507
+ :return: A dict[ChildType, ModelType]. If an input model doesn't have a child of the given child type,
508
+ then it will not be in the resulting dictionary.
509
+ """
510
+ to_child: dict[RecordModel, WrappedType] = RecordHandler.map_to_child(models, child_type)
511
+ by_child: dict[WrappedType, RecordModel] = {}
512
+ for record, child in to_child.items():
513
+ if child is None:
514
+ continue
515
+ if child in by_child:
516
+ raise SapioException(f"Child {child.data_type_name} {child.record_id} encountered more than once "
517
+ f"in models list.")
518
+ by_child[child] = record
519
+ return by_child
520
+
382
521
  @staticmethod
383
522
  def map_by_children(models: Iterable[RecordModel], child_type: type[WrappedType]) \
384
523
  -> dict[WrappedType, list[RecordModel]]:
@@ -416,8 +555,8 @@ class RecordHandler:
416
555
  return return_dict
417
556
 
418
557
  @staticmethod
419
- def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: str,
420
- side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
558
+ def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name: str,
559
+ side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
421
560
  """
422
561
  Take a list of record models and map them by their forward side link. Essentially an inversion of
423
562
  map_to_forward_side_link. Input models that share a forward side link will end up in the same list.
@@ -429,18 +568,44 @@ class RecordHandler:
429
568
  :return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
430
569
  pointing to it, then it will not be in the resulting dictionary.
431
570
  """
432
- to_side_link: dict[RecordModel, WrappedType] = RecordHandler\
571
+ to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
433
572
  .map_to_forward_side_link(models, field_name, side_link_type)
434
- by_side_link: dict[WrappedType, list[RecordModel]] = {}
573
+ by_side_link: dict[WrappedType, list[WrappedRecordModel]] = {}
435
574
  for record, side_link in to_side_link.items():
436
575
  if side_link is None:
437
576
  continue
438
577
  by_side_link.setdefault(side_link, []).append(record)
439
578
  return by_side_link
440
579
 
580
+ @staticmethod
581
+ def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: str,
582
+ side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
583
+ """
584
+ Take a list of record models and map them by their forward side link. Essentially an inversion of
585
+ map_to_forward_side_link, but if two records share the same forward link, an exception is thrown.
586
+ The forward side link must already be loaded.
587
+
588
+ :param models: A list of record models.
589
+ :param field_name: The field name on the record models where the side link is located.
590
+ :param side_link_type: The record model wrapper of the forward side links.
591
+ :return: A dict[SideLink, ModelType]. If an input model doesn't have a forward side link of the given type
592
+ pointing to it, then it will not be in the resulting dictionary.
593
+ """
594
+ to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
595
+ .map_to_forward_side_link(models, field_name, side_link_type)
596
+ by_side_link: dict[WrappedType, WrappedRecordModel] = {}
597
+ for record, side_link in to_side_link.items():
598
+ if side_link is None:
599
+ continue
600
+ if side_link in by_side_link:
601
+ raise SapioException(f"Side link {side_link.data_type_name} {side_link.record_id} encountered more "
602
+ f"than once in models list.")
603
+ by_side_link[side_link] = record
604
+ return by_side_link
605
+
441
606
  @staticmethod
442
607
  def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: str,
443
- side_link_type: type[WrappedType]) -> dict[RecordModel, list[WrappedRecordModel]]:
608
+ side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, list[WrappedType]]:
444
609
  """
445
610
  Map a list of record models to a list reverse side links of a given type. The reverse side links must already
446
611
  be loaded.
@@ -457,6 +622,29 @@ class RecordHandler:
457
622
  return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
458
623
  return return_dict
459
624
 
625
+ @staticmethod
626
+ def map_to_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: str,
627
+ side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
628
+ """
629
+ Map a list of record models to the reverse side link of a given type. If a given record has more than one
630
+ reverse side link of this type, an exception is thrown. The reverse side links must already be loaded.
631
+
632
+ :param models: A list of record models.
633
+ :param field_name: The field name on the side linked model where the side link to the given record models is
634
+ located.
635
+ :param side_link_type: The record model wrapper of the reverse side links.
636
+ :return: A dict[ModelType, SideLink]. If an input model doesn't have reverse side links of the given type,
637
+ then it will map to None.
638
+ """
639
+ return_dict: dict[WrappedRecordModel, WrappedType] = {}
640
+ for model in models:
641
+ links: list[WrappedType] = model.get_reverse_side_link(field_name, side_link_type)
642
+ if len(links) > 1:
643
+ raise SapioException(f"Model {model.data_type_name} {model.record_id} has more than one reverse link "
644
+ f"of type {side_link_type.get_wrapper_data_type_name()}.")
645
+ return_dict[model] = links[0] if links else None
646
+ return return_dict
647
+
460
648
  @staticmethod
461
649
  def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: str,
462
650
  side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
@@ -472,14 +660,41 @@ class RecordHandler:
472
660
  :return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
473
661
  pointing to it, then it will not be in the resulting dictionary.
474
662
  """
475
- to_side_links: dict[RecordModel, list[WrappedType]] = RecordHandler\
663
+ to_side_links: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler\
476
664
  .map_to_reverse_side_links(models, field_name, side_link_type)
477
- by_side_links: dict[WrappedType, list[RecordModel]] = {}
665
+ by_side_links: dict[WrappedType, list[WrappedRecordModel]] = {}
478
666
  for record, side_links in to_side_links.items():
479
667
  for side_link in side_links:
480
668
  by_side_links.setdefault(side_link, []).append(record)
481
669
  return by_side_links
482
670
 
671
+ @staticmethod
672
+ def map_by_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: str,
673
+ side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
674
+ """
675
+ Take a list of record models and map them by their reverse side link. Essentially an inversion of
676
+ map_to_reverse_side_link. If two records share the same reverse side link, an exception is thrown.
677
+ The reverse side links must already be loaded.
678
+
679
+ :param models: A list of record models.
680
+ :param field_name: The field name on the side linked model where the side link to the given record models is
681
+ located.
682
+ :param side_link_type: The record model wrapper of the reverse side links.
683
+ :return: A dict[SideLink, ModelType]. If an input model doesn't have a reverse side link of the given type
684
+ pointing to it, then it will not be in the resulting dictionary.
685
+ """
686
+ to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
687
+ .map_to_reverse_side_link(models, field_name, side_link_type)
688
+ by_side_link: dict[WrappedType, WrappedRecordModel] = {}
689
+ for record, side_link in to_side_link.items():
690
+ if side_link is None:
691
+ continue
692
+ if side_link in by_side_link:
693
+ raise SapioException(f"Side link {side_link.data_type_name} {side_link.record_id} encountered more "
694
+ f"than once in models list.")
695
+ by_side_link[side_link] = record
696
+ return by_side_link
697
+
483
698
  @staticmethod
484
699
  def map_by_id(models: Iterable[SapioRecord]) -> dict[int, SapioRecord]:
485
700
  """
@@ -690,7 +905,7 @@ class RecordHandler:
690
905
  relationship path must already be loaded.
691
906
 
692
907
  The path is "flattened" by only following the first record at each step. Useful for traversing 1-to-Many-to-1
693
- relationships (e.g. a sample with is aliquoted to a number of samples, then those aliquots are pooled back
908
+ relationships (e.g. a sample which is aliquoted to a number of samples, then those aliquots are pooled back
694
909
  together into a single sample).
695
910
 
696
911
  Currently, the relationship path may only contain parent/child nodes.
@@ -719,10 +934,28 @@ class RecordHandler:
719
934
  ret_dict.update({model: self.inst_man.wrap(current[0], wrapper_type) if current else None})
720
935
  return ret_dict
721
936
 
722
- def __exhaust_query_pages(self, data_type_name: str, field: str, value_list: list[Any],
723
- paging_criteria: DataRecordPojoPageCriteria | None,
724
- page_limit: int | None) \
725
- -> tuple[list[DataRecord], DataRecordPojoPageCriteria | None]:
726
- pager = QueryDataRecordsAutoPager(data_type_name, field, value_list, self.user, paging_criteria)
727
- pager.max_page = page_limit
728
- return pager.get_all_at_once(), pager.next_page_criteria
937
+ def __find_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value: Any,
938
+ secondary_identifiers: FieldMap | None = None) -> WrappedType | None:
939
+ """
940
+ Find a record from the system that matches the given field values. The primary identifier and value is used
941
+ to query for the record, then the secondary identifiers may be optionally provided to further filter the
942
+ returned results. If no record is found with these filters, returns None.
943
+ """
944
+ # Query for all records that match the primary identifier.
945
+ results: list[WrappedType] = self.query_models(wrapper_type, primary_identifier, [id_value])
946
+
947
+ # Find the one record, if any, that matches the secondary identifiers.
948
+ unique_record: WrappedType | None = None
949
+ for result in results:
950
+ matches_all: bool = True
951
+ for field, value in secondary_identifiers.items():
952
+ if result.get_field_value(field) != value:
953
+ matches_all = False
954
+ break
955
+ if matches_all:
956
+ # If a previous record in the results already matched all identifiers, then throw an exception.
957
+ if unique_record is not None:
958
+ raise SapioException(f"More than one record of type {wrapper_type.get_wrapper_data_type_name()} "
959
+ f"encountered in system that matches all provided identifiers.")
960
+ unique_record = result
961
+ return unique_record
@@ -47,6 +47,7 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
47
47
  # Wrap the execution of each webhook in a try/catch. If an exception occurs, handle any special sapiopycommons
48
48
  # exceptions. Otherwise, return a generic message stating that an error occurred.
49
49
  try:
50
+ self.initialize(context)
50
51
  return self.execute(context)
51
52
  except SapioUserErrorException as e:
52
53
  return self.handle_user_error_exception(e)
@@ -57,6 +58,13 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
57
58
  except Exception as e:
58
59
  return self.handle_unexpected_exception(e)
59
60
 
61
+ def initialize(self, context: SapioWebhookContext) -> None:
62
+ """
63
+ A function that can be optionally overridden by your webhooks to initialize additional instance variables,
64
+ or set up whatever else you wish to set up before the execute function is ran. Default behavior does nothing.
65
+ """
66
+ pass
67
+
60
68
  @abstractmethod
61
69
  def execute(self, context: SapioWebhookContext) -> SapioWebhookResult:
62
70
  """
@@ -91,7 +99,8 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
91
99
  if result is not None:
92
100
  return result
93
101
  self.log_error(traceback.format_exc())
94
- DataMgmtServer.get_client_callback(self.context.user).display_error(e.args[0])
102
+ if self.can_send_client_callback():
103
+ DataMgmtServer.get_client_callback(self.context.user).display_error(e.args[0])
95
104
  return SapioWebhookResult(False)
96
105
 
97
106
  def handle_unexpected_exception(self, e: Exception) -> SapioWebhookResult:
@@ -237,3 +246,51 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
237
246
  :return: True if this endpoint was invoked as a scheduled action.
238
247
  """
239
248
  return self.context.end_point_type == WebhookEndpointType.SCHEDULEDPLUGIN
249
+
250
+ def is_action_button_field(self) -> bool:
251
+ """
252
+ :return: True if this endpoint was invoked as an action button field.
253
+ """
254
+ return self.context.end_point_type == WebhookEndpointType.ACTIONDATAFIELD
255
+
256
+ def is_action_text_field(self) -> bool:
257
+ """
258
+ :return: True if this endpoint was invoked as an action text field.
259
+ """
260
+ return self.context.end_point_type == WebhookEndpointType.ACTION_TEXT_FIELD
261
+
262
+ def is_custom(self) -> bool:
263
+ """
264
+ :return: True if this endpoint was invoked from a custom point, such as a custom queue.
265
+ """
266
+ return self.context.end_point_type == WebhookEndpointType.CUSTOM
267
+
268
+ def is_calendar_event_click_handler(self) -> bool:
269
+ """
270
+ :return: True if this endpoint was invoked from a calendar event click handler.
271
+ """
272
+ return self.context.end_point_type == WebhookEndpointType.CALENDAR_EVENT_CLICK_HANDLER
273
+
274
+ def is_eln_menu_grabber(self) -> bool:
275
+ """
276
+ :return: True if this endpoint was invoked as a notebook entry grabber.
277
+ """
278
+ return self.context.end_point_type == WebhookEndpointType.NOTEBOOKEXPERIMENTGRABBER
279
+
280
+ def is_conversation_bot(self) -> bool:
281
+ """
282
+ :return: True if this endpoint was invoked as from a conversation bot.
283
+ """
284
+ return self.context.end_point_type == WebhookEndpointType.CONVERSATION_BOT
285
+
286
+ def is_multi_data_type_table_toolbar(self) -> bool:
287
+ """
288
+ :return: True if this endpoint was invoked as a multi data type table toolbar button.
289
+ """
290
+ return self.context.end_point_type == WebhookEndpointType.REPORTTOOLBAR
291
+
292
+ def can_send_client_callback(self) -> bool:
293
+ """
294
+ :return: Whether client callbacks and directives can be sent from this webhook's endpoint type.
295
+ """
296
+ return self.context.is_client_callback_available
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2024.8.28a313
3
+ Version: 2024.8.28a314
4
4
  Summary: Official Sapio Python API Utilities Package
5
5
  Project-URL: Homepage, https://github.com/sapiosciences
6
6
  Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
@@ -17,7 +17,8 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
18
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
19
  Requires-Python: >=3.10
20
- Requires-Dist: sapiopylib>=2023.12.13.174
20
+ Requires-Dist: databind>=4.5
21
+ Requires-Dist: sapiopylib>=2024.5.24.210
21
22
  Description-Content-Type: text/markdown
22
23
 
23
24
 
@@ -50,6 +51,7 @@ This license does not provide any rights to use any other copyrighted artifacts
50
51
  ## Dependencies
51
52
  The following dependencies are required for this package:
52
53
  - [sapiopylib - The official Sapio Informatics Platform Python API package.](https://pypi.org/project/sapiopylib/)
54
+ - [databind - Databind is a library inspired by jackson-databind to de-/serialize Python dataclasses.](https://pypi.org/project/databind/)
53
55
 
54
56
  ## Getting Help
55
57
  If you have a support contract with Sapio Sciences, please use our [technical support channels](https://sapio-sciences.atlassian.net/servicedesk/customer/portals).
@@ -1,38 +1,43 @@
1
1
  sapiopycommons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  sapiopycommons/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- sapiopycommons/callbacks/callback_util.py,sha256=D6whWxYWvs5rXOG2Slpi1icC18SLKmG9-MP9f0YDDNE,43256
4
- sapiopycommons/chem/IndigoMolecules.py,sha256=ukZcX6TMEgkNdD1L1GnH3tp5rGplFNPlGoChAHXbsxw,1945
5
- sapiopycommons/chem/Molecules.py,sha256=tOkn3fg4QizgqjkRLuvRdVy0JpTD3QEOSvZPxmIyT4c,8607
3
+ sapiopycommons/callbacks/callback_util.py,sha256=caeIWCHvK33jDs3TRskpJv0kDe7W8NPK4MyJPjgztwo,58012
4
+ sapiopycommons/chem/IndigoMolecules.py,sha256=QqFDi9CKERj6sn_ZwVcS2xZq4imlkaTeCrpq1iNcEJA,1992
5
+ sapiopycommons/chem/Molecules.py,sha256=t80IsQBPJ9mwE8ZxnWomAGrZDhdsOuPvLaTPb_N6jGU,8639
6
6
  sapiopycommons/chem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  sapiopycommons/datatype/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  sapiopycommons/datatype/attachment_util.py,sha256=YlnMprj5IGBbAZDLG2khS1P7JIYTw_NYfpJAfRZfP3M,3219
9
9
  sapiopycommons/eln/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  sapiopycommons/eln/experiment_handler.py,sha256=v1pG4qtZb8OSNWfKtFo6NjnEkReqnu5R9i_hqWh_xxg,57198
11
+ sapiopycommons/eln/experiment_report_util.py,sha256=FTLw-6SLAMeoWTOO-qhGROE9g54pZdyoQJIhiIzlwGw,7848
11
12
  sapiopycommons/eln/plate_designer.py,sha256=FYJfhhNq8hdfuXgDYOYHy6g0m2zNwQXZWF_MTPzElDg,7184
12
13
  sapiopycommons/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
14
  sapiopycommons/files/complex_data_loader.py,sha256=XSJOl676mIklJo78v07-70u1b015a5DI4sqZPI3C-Tw,1475
14
- sapiopycommons/files/file_bridge.py,sha256=6yjUi0ejypb1nvcEvn21EuquB-SmEjB-fCZiMaNZg7Q,5757
15
+ sapiopycommons/files/file_bridge.py,sha256=GI3-gWFzcL0q0c8jKOxTevbzJqtUpiElmkXfTnMsaOo,6224
16
+ sapiopycommons/files/file_bridge_handler.py,sha256=MU2wZR4VY606yx6Bnv8-LzG3mGCeuXeRBn914WNRFCo,13601
15
17
  sapiopycommons/files/file_data_handler.py,sha256=3-guAdhJdeJWAFq1a27ijspkO7uMMZ6CapMCD_6o4jA,36746
16
- sapiopycommons/files/file_util.py,sha256=92SzwRif4dOcGqZ9ri90QeC20NOCenT8DxQjdSH5Uyc,25556
17
- sapiopycommons/files/file_validator.py,sha256=5DUI_h0WB1AvfoPgx8En3-sC5xlzm5Z2deoSf9qviKQ,24499
18
+ sapiopycommons/files/file_util.py,sha256=44mzhn3M_QltoncBB-ooX7_yO6u5k-XU_bzUXHGxUiw,26299
19
+ sapiopycommons/files/file_validator.py,sha256=BhXB2XnoNEzdBXuwul1s2RNoj-3ZoiMmephUCU_0o3Y,28113
18
20
  sapiopycommons/files/file_writer.py,sha256=5u_iZXTQvuUU7ceHZr8Q001_tvgJhOqBwAnB_pxcAbQ,16027
19
21
  sapiopycommons/general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ sapiopycommons/general/accession_service.py,sha256=HYgyOsH_UaoRnoury-c2yTW8SeG4OtjLemdpCzoV4R8,13484
20
23
  sapiopycommons/general/aliases.py,sha256=i6af5o2oVFGNcyk7GkvTWXQs0H9xTbFKc_GIah8NKVU,3594
21
- sapiopycommons/general/custom_report_util.py,sha256=Yrq-Ize1M1jh9g3BmQT9Egedufi3Nl9xNmgNI_LGiho,4828
24
+ sapiopycommons/general/custom_report_util.py,sha256=cLgIR5Fn3M9uyAtgfTYRv3JRk2SKNevnsb_R5zidSYs,15557
22
25
  sapiopycommons/general/exceptions.py,sha256=DOlLKnpCatxQF-lVCToa8ryJgusWLvip6N_1ALN00QE,1679
23
26
  sapiopycommons/general/popup_util.py,sha256=-mN5IgYPrLrOEHJ4CHPi2rec4_WAN6X0yMxHwD5h3Bs,30126
24
27
  sapiopycommons/general/storage_util.py,sha256=ovmK_jN7v09BoX07XxwShpBUC5WYQOM7dbKV_VeLXJU,8892
25
28
  sapiopycommons/general/time_util.py,sha256=jiJUh7jc1ZRCOem880S3HaLPZ4RboBtSl4_U9sqAQuM,7290
29
+ sapiopycommons/multimodal/multimodal.py,sha256=A1QsC8QTPmgZyPr7KtMbPRedn2Ie4WIErodUvQ9otgU,6724
30
+ sapiopycommons/multimodal/multimodal_data.py,sha256=zqgYHO-ULaPKV0POFWZVY9N-Sfm1RQWwdsfwFxe5DjI,15038
26
31
  sapiopycommons/processtracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
32
  sapiopycommons/processtracking/endpoints.py,sha256=g5h_uCVByqacYm9zWAz8TyAdRsGfaO2o0b5RSJdOaSA,10926
28
33
  sapiopycommons/recordmodel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
- sapiopycommons/recordmodel/record_handler.py,sha256=VYUJ0bgZbyc6-XYRKvsxrpWHLdCwxzhv13Ce2tZpAQQ,39348
34
+ sapiopycommons/recordmodel/record_handler.py,sha256=AyK1H3x-g1eu1Mt9XD1h57yRrZp_TJjZlEaQ2kPP4Dc,54432
30
35
  sapiopycommons/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
36
  sapiopycommons/rules/eln_rule_handler.py,sha256=qfkBZtck0KK1i9s9Xe2UZqkzQOgPCzDxRkhxE8Si1xk,10671
32
37
  sapiopycommons/rules/on_save_rule_handler.py,sha256=JY9F30IcHwFVdgPAMQtTYuRastV1jeezhVktyrzNASU,10763
33
38
  sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- sapiopycommons/webhook/webhook_handlers.py,sha256=K_K7CEAMZ-bNb2LCIKdt0CxHsBKkwSBzfnp0JSdGJUM,11102
35
- sapiopycommons-2024.8.28a313.dist-info/METADATA,sha256=xb9YgxmbGyTKDDaq4vOrxEk5wNz-8KQ_-6WPBgMFexg,3009
36
- sapiopycommons-2024.8.28a313.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
37
- sapiopycommons-2024.8.28a313.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
38
- sapiopycommons-2024.8.28a313.dist-info/RECORD,,
39
+ sapiopycommons/webhook/webhook_handlers.py,sha256=ibpBY3Sk3Eij919bIdW0awzlogYoQSWYDDOg--NwsQE,13431
40
+ sapiopycommons-2024.8.28a314.dist-info/METADATA,sha256=pZYfrhMqAs3EgS-_J1vPfJH9t-ZTyeKp8JhmHKgHqOI,3176
41
+ sapiopycommons-2024.8.28a314.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
42
+ sapiopycommons-2024.8.28a314.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
43
+ sapiopycommons-2024.8.28a314.dist-info/RECORD,,