sapiopycommons 2024.8.27a312__py3-none-any.whl → 2024.8.28a313__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sapiopycommons might be problematic. Click here for more details.
- sapiopycommons/callbacks/callback_util.py +35 -277
- sapiopycommons/chem/IndigoMolecules.py +0 -1
- sapiopycommons/chem/Molecules.py +0 -1
- sapiopycommons/files/file_bridge.py +10 -16
- sapiopycommons/files/file_util.py +6 -13
- sapiopycommons/files/file_validator.py +0 -71
- sapiopycommons/general/custom_report_util.py +27 -199
- sapiopycommons/recordmodel/record_handler.py +45 -278
- sapiopycommons/webhook/webhook_handlers.py +1 -58
- {sapiopycommons-2024.8.27a312.dist-info → sapiopycommons-2024.8.28a313.dist-info}/METADATA +2 -4
- {sapiopycommons-2024.8.27a312.dist-info → sapiopycommons-2024.8.28a313.dist-info}/RECORD +13 -18
- sapiopycommons/eln/experiment_report_util.py +0 -214
- sapiopycommons/files/file_bridge_handler.py +0 -318
- sapiopycommons/general/accession_service.py +0 -375
- sapiopycommons/multimodal/multimodal.py +0 -146
- sapiopycommons/multimodal/multimodal_data.py +0 -487
- {sapiopycommons-2024.8.27a312.dist-info → sapiopycommons-2024.8.28a313.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.8.27a312.dist-info → sapiopycommons-2024.8.28a313.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,10 +3,8 @@ 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
|
|
7
6
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
8
7
|
from sapiopylib.rest.pojo.DataRecordPaging import DataRecordPojoPageCriteria
|
|
9
|
-
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
10
8
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
11
9
|
from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
|
|
12
10
|
QueryAllRecordsOfTypeAutoPager
|
|
@@ -77,43 +75,6 @@ class RecordHandler:
|
|
|
77
75
|
"""
|
|
78
76
|
return self.query_models_with_criteria(wrapper_type, field, value_list, None, page_limit)[0]
|
|
79
77
|
|
|
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
|
-
|
|
117
78
|
def query_models_with_criteria(self, wrapper_type: type[WrappedType], field: str, value_list: Iterable[Any],
|
|
118
79
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
119
80
|
page_limit: int | None = None) \
|
|
@@ -199,58 +160,24 @@ class RecordHandler:
|
|
|
199
160
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
200
161
|
|
|
201
162
|
def query_models_by_report(self, wrapper_type: type[WrappedType],
|
|
202
|
-
report_name: str
|
|
163
|
+
report_name: str,
|
|
203
164
|
filters: dict[str, Iterable[Any]] | None = None,
|
|
204
|
-
page_limit: int | None = None
|
|
205
|
-
page_size: int | None = None,
|
|
206
|
-
page_number: int | None = None) -> list[WrappedType]:
|
|
165
|
+
page_limit: int | None = None) -> list[WrappedType]:
|
|
207
166
|
"""
|
|
208
|
-
Run a report
|
|
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.
|
|
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.
|
|
213
169
|
|
|
214
|
-
|
|
170
|
+
Will throw an exception if the given system report does not have a RecordId column.
|
|
215
171
|
|
|
216
172
|
:param wrapper_type: The record model wrapper to use.
|
|
217
|
-
:param report_name: The name of
|
|
218
|
-
criteria for a custom report.
|
|
173
|
+
:param report_name: The name of the system report to run.
|
|
219
174
|
:param filters: If provided, filter the results of the report using the given mapping of headers to values to
|
|
220
175
|
filter on. This filtering is done before the records are queried.
|
|
221
176
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
222
|
-
:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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.
|
|
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.
|
|
254
181
|
ids: list[int] = [row["RecordId"] for row in results]
|
|
255
182
|
return self.query_models_by_id(wrapper_type, ids)
|
|
256
183
|
|
|
@@ -284,46 +211,10 @@ class RecordHandler:
|
|
|
284
211
|
the fields in the fields list.
|
|
285
212
|
"""
|
|
286
213
|
models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
|
|
287
|
-
for model,
|
|
288
|
-
model.set_field_values(
|
|
214
|
+
for model, field in zip(models, fields):
|
|
215
|
+
model.set_field_values(field)
|
|
289
216
|
return models
|
|
290
217
|
|
|
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
|
-
|
|
327
218
|
def create_models(self, wrapper_type: type[WrappedType], num: int) -> list[WrappedType]:
|
|
328
219
|
"""
|
|
329
220
|
Shorthand for creating new records via the data record manager and then returning them as wrapped
|
|
@@ -379,8 +270,24 @@ class RecordHandler:
|
|
|
379
270
|
if secondary_identifiers is None:
|
|
380
271
|
secondary_identifiers = {}
|
|
381
272
|
|
|
382
|
-
|
|
383
|
-
|
|
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
|
+
|
|
384
291
|
# If a unique record matched the identifiers, return it.
|
|
385
292
|
if unique_record is not None:
|
|
386
293
|
return unique_record
|
|
@@ -422,29 +329,6 @@ class RecordHandler:
|
|
|
422
329
|
return_dict[model] = model.get_parents_of_type(parent_type)
|
|
423
330
|
return return_dict
|
|
424
331
|
|
|
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
|
-
|
|
448
332
|
@staticmethod
|
|
449
333
|
def map_by_parents(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
|
|
450
334
|
-> dict[WrappedType, list[RecordModel]]:
|
|
@@ -495,29 +379,6 @@ class RecordHandler:
|
|
|
495
379
|
return_dict[model] = model.get_children_of_type(child_type)
|
|
496
380
|
return return_dict
|
|
497
381
|
|
|
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
|
-
|
|
521
382
|
@staticmethod
|
|
522
383
|
def map_by_children(models: Iterable[RecordModel], child_type: type[WrappedType]) \
|
|
523
384
|
-> dict[WrappedType, list[RecordModel]]:
|
|
@@ -555,8 +416,8 @@ class RecordHandler:
|
|
|
555
416
|
return return_dict
|
|
556
417
|
|
|
557
418
|
@staticmethod
|
|
558
|
-
def
|
|
559
|
-
|
|
419
|
+
def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: str,
|
|
420
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
560
421
|
"""
|
|
561
422
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
562
423
|
map_to_forward_side_link. Input models that share a forward side link will end up in the same list.
|
|
@@ -568,44 +429,18 @@ class RecordHandler:
|
|
|
568
429
|
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
|
|
569
430
|
pointing to it, then it will not be in the resulting dictionary.
|
|
570
431
|
"""
|
|
571
|
-
to_side_link: dict[
|
|
432
|
+
to_side_link: dict[RecordModel, WrappedType] = RecordHandler\
|
|
572
433
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
573
|
-
by_side_link: dict[WrappedType, list[
|
|
434
|
+
by_side_link: dict[WrappedType, list[RecordModel]] = {}
|
|
574
435
|
for record, side_link in to_side_link.items():
|
|
575
436
|
if side_link is None:
|
|
576
437
|
continue
|
|
577
438
|
by_side_link.setdefault(side_link, []).append(record)
|
|
578
439
|
return by_side_link
|
|
579
440
|
|
|
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
|
-
|
|
606
441
|
@staticmethod
|
|
607
442
|
def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: str,
|
|
608
|
-
side_link_type: type[WrappedType]) -> dict[
|
|
443
|
+
side_link_type: type[WrappedType]) -> dict[RecordModel, list[WrappedRecordModel]]:
|
|
609
444
|
"""
|
|
610
445
|
Map a list of record models to a list reverse side links of a given type. The reverse side links must already
|
|
611
446
|
be loaded.
|
|
@@ -622,29 +457,6 @@ class RecordHandler:
|
|
|
622
457
|
return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
|
|
623
458
|
return return_dict
|
|
624
459
|
|
|
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
|
-
|
|
648
460
|
@staticmethod
|
|
649
461
|
def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: str,
|
|
650
462
|
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
@@ -660,41 +472,14 @@ class RecordHandler:
|
|
|
660
472
|
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
|
|
661
473
|
pointing to it, then it will not be in the resulting dictionary.
|
|
662
474
|
"""
|
|
663
|
-
to_side_links: dict[
|
|
475
|
+
to_side_links: dict[RecordModel, list[WrappedType]] = RecordHandler\
|
|
664
476
|
.map_to_reverse_side_links(models, field_name, side_link_type)
|
|
665
|
-
by_side_links: dict[WrappedType, list[
|
|
477
|
+
by_side_links: dict[WrappedType, list[RecordModel]] = {}
|
|
666
478
|
for record, side_links in to_side_links.items():
|
|
667
479
|
for side_link in side_links:
|
|
668
480
|
by_side_links.setdefault(side_link, []).append(record)
|
|
669
481
|
return by_side_links
|
|
670
482
|
|
|
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
|
-
|
|
698
483
|
@staticmethod
|
|
699
484
|
def map_by_id(models: Iterable[SapioRecord]) -> dict[int, SapioRecord]:
|
|
700
485
|
"""
|
|
@@ -905,7 +690,7 @@ class RecordHandler:
|
|
|
905
690
|
relationship path must already be loaded.
|
|
906
691
|
|
|
907
692
|
The path is "flattened" by only following the first record at each step. Useful for traversing 1-to-Many-to-1
|
|
908
|
-
relationships (e.g. a sample
|
|
693
|
+
relationships (e.g. a sample with is aliquoted to a number of samples, then those aliquots are pooled back
|
|
909
694
|
together into a single sample).
|
|
910
695
|
|
|
911
696
|
Currently, the relationship path may only contain parent/child nodes.
|
|
@@ -934,28 +719,10 @@ class RecordHandler:
|
|
|
934
719
|
ret_dict.update({model: self.inst_man.wrap(current[0], wrapper_type) if current else None})
|
|
935
720
|
return ret_dict
|
|
936
721
|
|
|
937
|
-
def
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
|
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
|
|
@@ -47,7 +47,6 @@ 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)
|
|
51
50
|
return self.execute(context)
|
|
52
51
|
except SapioUserErrorException as e:
|
|
53
52
|
return self.handle_user_error_exception(e)
|
|
@@ -58,13 +57,6 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
58
57
|
except Exception as e:
|
|
59
58
|
return self.handle_unexpected_exception(e)
|
|
60
59
|
|
|
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
|
-
|
|
68
60
|
@abstractmethod
|
|
69
61
|
def execute(self, context: SapioWebhookContext) -> SapioWebhookResult:
|
|
70
62
|
"""
|
|
@@ -99,8 +91,7 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
99
91
|
if result is not None:
|
|
100
92
|
return result
|
|
101
93
|
self.log_error(traceback.format_exc())
|
|
102
|
-
|
|
103
|
-
DataMgmtServer.get_client_callback(self.context.user).display_error(e.args[0])
|
|
94
|
+
DataMgmtServer.get_client_callback(self.context.user).display_error(e.args[0])
|
|
104
95
|
return SapioWebhookResult(False)
|
|
105
96
|
|
|
106
97
|
def handle_unexpected_exception(self, e: Exception) -> SapioWebhookResult:
|
|
@@ -246,51 +237,3 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
246
237
|
:return: True if this endpoint was invoked as a scheduled action.
|
|
247
238
|
"""
|
|
248
239
|
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.
|
|
3
|
+
Version: 2024.8.28a313
|
|
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,8 +17,7 @@ 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:
|
|
21
|
-
Requires-Dist: sapiopylib>=2024.5.24.210
|
|
20
|
+
Requires-Dist: sapiopylib>=2023.12.13.174
|
|
22
21
|
Description-Content-Type: text/markdown
|
|
23
22
|
|
|
24
23
|
|
|
@@ -51,7 +50,6 @@ This license does not provide any rights to use any other copyrighted artifacts
|
|
|
51
50
|
## Dependencies
|
|
52
51
|
The following dependencies are required for this package:
|
|
53
52
|
- [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/)
|
|
55
53
|
|
|
56
54
|
## Getting Help
|
|
57
55
|
If you have a support contract with Sapio Sciences, please use our [technical support channels](https://sapio-sciences.atlassian.net/servicedesk/customer/portals).
|
|
@@ -1,43 +1,38 @@
|
|
|
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=
|
|
4
|
-
sapiopycommons/chem/IndigoMolecules.py,sha256=
|
|
5
|
-
sapiopycommons/chem/Molecules.py,sha256=
|
|
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
|
|
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
|
|
12
11
|
sapiopycommons/eln/plate_designer.py,sha256=FYJfhhNq8hdfuXgDYOYHy6g0m2zNwQXZWF_MTPzElDg,7184
|
|
13
12
|
sapiopycommons/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
13
|
sapiopycommons/files/complex_data_loader.py,sha256=XSJOl676mIklJo78v07-70u1b015a5DI4sqZPI3C-Tw,1475
|
|
15
|
-
sapiopycommons/files/file_bridge.py,sha256=
|
|
16
|
-
sapiopycommons/files/file_bridge_handler.py,sha256=MU2wZR4VY606yx6Bnv8-LzG3mGCeuXeRBn914WNRFCo,13601
|
|
14
|
+
sapiopycommons/files/file_bridge.py,sha256=6yjUi0ejypb1nvcEvn21EuquB-SmEjB-fCZiMaNZg7Q,5757
|
|
17
15
|
sapiopycommons/files/file_data_handler.py,sha256=3-guAdhJdeJWAFq1a27ijspkO7uMMZ6CapMCD_6o4jA,36746
|
|
18
|
-
sapiopycommons/files/file_util.py,sha256=
|
|
19
|
-
sapiopycommons/files/file_validator.py,sha256=
|
|
16
|
+
sapiopycommons/files/file_util.py,sha256=92SzwRif4dOcGqZ9ri90QeC20NOCenT8DxQjdSH5Uyc,25556
|
|
17
|
+
sapiopycommons/files/file_validator.py,sha256=5DUI_h0WB1AvfoPgx8En3-sC5xlzm5Z2deoSf9qviKQ,24499
|
|
20
18
|
sapiopycommons/files/file_writer.py,sha256=5u_iZXTQvuUU7ceHZr8Q001_tvgJhOqBwAnB_pxcAbQ,16027
|
|
21
19
|
sapiopycommons/general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
sapiopycommons/general/accession_service.py,sha256=HYgyOsH_UaoRnoury-c2yTW8SeG4OtjLemdpCzoV4R8,13484
|
|
23
20
|
sapiopycommons/general/aliases.py,sha256=i6af5o2oVFGNcyk7GkvTWXQs0H9xTbFKc_GIah8NKVU,3594
|
|
24
|
-
sapiopycommons/general/custom_report_util.py,sha256=
|
|
21
|
+
sapiopycommons/general/custom_report_util.py,sha256=Yrq-Ize1M1jh9g3BmQT9Egedufi3Nl9xNmgNI_LGiho,4828
|
|
25
22
|
sapiopycommons/general/exceptions.py,sha256=DOlLKnpCatxQF-lVCToa8ryJgusWLvip6N_1ALN00QE,1679
|
|
26
23
|
sapiopycommons/general/popup_util.py,sha256=-mN5IgYPrLrOEHJ4CHPi2rec4_WAN6X0yMxHwD5h3Bs,30126
|
|
27
24
|
sapiopycommons/general/storage_util.py,sha256=ovmK_jN7v09BoX07XxwShpBUC5WYQOM7dbKV_VeLXJU,8892
|
|
28
25
|
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
|
|
31
26
|
sapiopycommons/processtracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
27
|
sapiopycommons/processtracking/endpoints.py,sha256=g5h_uCVByqacYm9zWAz8TyAdRsGfaO2o0b5RSJdOaSA,10926
|
|
33
28
|
sapiopycommons/recordmodel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
-
sapiopycommons/recordmodel/record_handler.py,sha256=
|
|
29
|
+
sapiopycommons/recordmodel/record_handler.py,sha256=VYUJ0bgZbyc6-XYRKvsxrpWHLdCwxzhv13Ce2tZpAQQ,39348
|
|
35
30
|
sapiopycommons/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
31
|
sapiopycommons/rules/eln_rule_handler.py,sha256=qfkBZtck0KK1i9s9Xe2UZqkzQOgPCzDxRkhxE8Si1xk,10671
|
|
37
32
|
sapiopycommons/rules/on_save_rule_handler.py,sha256=JY9F30IcHwFVdgPAMQtTYuRastV1jeezhVktyrzNASU,10763
|
|
38
33
|
sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
|
-
sapiopycommons/webhook/webhook_handlers.py,sha256=
|
|
40
|
-
sapiopycommons-2024.8.
|
|
41
|
-
sapiopycommons-2024.8.
|
|
42
|
-
sapiopycommons-2024.8.
|
|
43
|
-
sapiopycommons-2024.8.
|
|
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,,
|