sapiopycommons 2024.8.15a304__py3-none-any.whl → 2024.8.20a306__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 +130 -34
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +60 -0
- sapiopycommons/customreport/custom_report_builder.py +125 -0
- sapiopycommons/customreport/term_builder.py +299 -0
- sapiopycommons/datatype/attachment_util.py +11 -10
- sapiopycommons/eln/experiment_handler.py +209 -44
- sapiopycommons/eln/experiment_report_util.py +33 -129
- sapiopycommons/files/complex_data_loader.py +5 -4
- sapiopycommons/files/file_bridge.py +15 -14
- sapiopycommons/files/file_bridge_handler.py +26 -4
- sapiopycommons/files/file_data_handler.py +2 -5
- sapiopycommons/files/file_util.py +38 -5
- sapiopycommons/files/file_validator.py +26 -11
- sapiopycommons/files/file_writer.py +44 -15
- sapiopycommons/general/aliases.py +147 -3
- sapiopycommons/general/audit_log.py +196 -0
- sapiopycommons/general/custom_report_util.py +34 -32
- sapiopycommons/general/popup_util.py +17 -0
- sapiopycommons/general/sapio_links.py +50 -0
- sapiopycommons/general/time_util.py +40 -0
- sapiopycommons/multimodal/multimodal_data.py +0 -1
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +183 -61
- sapiopycommons/rules/eln_rule_handler.py +34 -25
- sapiopycommons/rules/on_save_rule_handler.py +34 -31
- sapiopycommons/webhook/webhook_handlers.py +90 -26
- sapiopycommons/webhook/webservice_handlers.py +67 -0
- {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.20a306.dist-info}/METADATA +1 -1
- sapiopycommons-2024.8.20a306.dist-info/RECORD +50 -0
- sapiopycommons-2024.8.15a304.dist-info/RECORD +0 -43
- {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.20a306.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.20a306.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from collections.abc import Iterable
|
|
2
|
-
from
|
|
4
|
+
from weakref import WeakValueDictionary
|
|
3
5
|
|
|
4
6
|
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
5
7
|
from sapiopylib.rest.User import SapioUser
|
|
@@ -7,7 +9,7 @@ from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, RawReportTer
|
|
|
7
9
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
8
10
|
from sapiopylib.rest.pojo.DataRecordPaging import DataRecordPojoPageCriteria
|
|
9
11
|
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
10
|
-
from sapiopylib.rest.pojo.
|
|
12
|
+
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
11
13
|
from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
|
|
12
14
|
QueryAllRecordsOfTypeAutoPager
|
|
13
15
|
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
@@ -16,8 +18,10 @@ from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelMana
|
|
|
16
18
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, WrappedRecordModel
|
|
17
19
|
from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
|
|
18
20
|
RelationshipNodeType
|
|
21
|
+
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
19
22
|
|
|
20
|
-
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap
|
|
23
|
+
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
|
|
24
|
+
FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey
|
|
21
25
|
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
22
26
|
from sapiopycommons.general.exceptions import SapioException
|
|
23
27
|
|
|
@@ -32,16 +36,38 @@ class RecordHandler:
|
|
|
32
36
|
rec_man: RecordModelManager
|
|
33
37
|
inst_man: RecordModelInstanceManager
|
|
34
38
|
rel_man: RecordModelRelationshipManager
|
|
39
|
+
an_man: RecordModelAncestorManager
|
|
40
|
+
|
|
41
|
+
__instances: WeakValueDictionary[SapioUser, RecordHandler] = WeakValueDictionary()
|
|
42
|
+
__initialized: bool
|
|
43
|
+
|
|
44
|
+
def __new__(cls, context: UserIdentifier):
|
|
45
|
+
"""
|
|
46
|
+
:param context: The current webhook context or a user object to send requests from.
|
|
47
|
+
"""
|
|
48
|
+
user = AliasUtil.to_sapio_user(context)
|
|
49
|
+
obj = cls.__instances.get(user)
|
|
50
|
+
if not obj:
|
|
51
|
+
obj = object.__new__(cls)
|
|
52
|
+
obj.__initialized = False
|
|
53
|
+
cls.__instances[user] = obj
|
|
54
|
+
return obj
|
|
35
55
|
|
|
36
|
-
def __init__(self, context:
|
|
56
|
+
def __init__(self, context: UserIdentifier):
|
|
37
57
|
"""
|
|
38
58
|
:param context: The current webhook context or a user object to send requests from.
|
|
39
59
|
"""
|
|
60
|
+
self.user = AliasUtil.to_sapio_user(context)
|
|
61
|
+
if self.__initialized:
|
|
62
|
+
return
|
|
63
|
+
self.__initialized = True
|
|
64
|
+
|
|
40
65
|
self.user = context if isinstance(context, SapioUser) else context.user
|
|
41
66
|
self.dr_man = DataRecordManager(self.user)
|
|
42
67
|
self.rec_man = RecordModelManager(self.user)
|
|
43
68
|
self.inst_man = self.rec_man.instance_manager
|
|
44
69
|
self.rel_man = self.rec_man.relationship_manager
|
|
70
|
+
self.an_man = RecordModelAncestorManager(self.rec_man)
|
|
45
71
|
|
|
46
72
|
def wrap_model(self, record: DataRecord, wrapper_type: type[WrappedType]) -> WrappedType:
|
|
47
73
|
"""
|
|
@@ -51,6 +77,7 @@ class RecordHandler:
|
|
|
51
77
|
:param wrapper_type: The record model wrapper to use.
|
|
52
78
|
:return: The record model for the input.
|
|
53
79
|
"""
|
|
80
|
+
self.__verify_data_type([record], wrapper_type)
|
|
54
81
|
return self.inst_man.add_existing_record_of_type(record, wrapper_type)
|
|
55
82
|
|
|
56
83
|
def wrap_models(self, records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> list[WrappedType]:
|
|
@@ -61,9 +88,10 @@ class RecordHandler:
|
|
|
61
88
|
:param wrapper_type: The record model wrapper to use.
|
|
62
89
|
:return: The record models for the input.
|
|
63
90
|
"""
|
|
91
|
+
self.__verify_data_type(records, wrapper_type)
|
|
64
92
|
return self.inst_man.add_existing_records_of_type(list(records), wrapper_type)
|
|
65
93
|
|
|
66
|
-
def query_models(self, wrapper_type: type[WrappedType], field:
|
|
94
|
+
def query_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier, value_list: Iterable[FieldValue],
|
|
67
95
|
page_limit: int | None = None) -> list[WrappedType]:
|
|
68
96
|
"""
|
|
69
97
|
Shorthand for using the data record manager to query for a list of data records by field value
|
|
@@ -77,9 +105,9 @@ class RecordHandler:
|
|
|
77
105
|
"""
|
|
78
106
|
return self.query_models_with_criteria(wrapper_type, field, value_list, None, page_limit)[0]
|
|
79
107
|
|
|
80
|
-
def query_and_map_models(self, wrapper_type: type[WrappedType], field:
|
|
81
|
-
|
|
82
|
-
|
|
108
|
+
def query_and_map_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
|
|
109
|
+
value_list: Iterable[FieldValue], page_limit: int | None = None,
|
|
110
|
+
*, mapping_field: FieldIdentifier | None = None) -> dict[FieldValue, list[WrappedType]]:
|
|
83
111
|
"""
|
|
84
112
|
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
85
113
|
map_by_field to turn the returned list into a dictionary mapping field values to records.
|
|
@@ -95,9 +123,10 @@ class RecordHandler:
|
|
|
95
123
|
mapping_field = field
|
|
96
124
|
return self.map_by_field(self.query_models(wrapper_type, field, value_list, page_limit), mapping_field)
|
|
97
125
|
|
|
98
|
-
def query_and_unique_map_models(self, wrapper_type: type[WrappedType], field:
|
|
99
|
-
|
|
100
|
-
|
|
126
|
+
def query_and_unique_map_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
|
|
127
|
+
value_list: Iterable[FieldValue], page_limit: int | None = None,
|
|
128
|
+
*, mapping_field: FieldIdentifier | None = None) \
|
|
129
|
+
-> dict[FieldValue, WrappedType]:
|
|
101
130
|
"""
|
|
102
131
|
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
103
132
|
map_by_unique_field to turn the returned list into a dictionary mapping field values to records.
|
|
@@ -114,7 +143,8 @@ class RecordHandler:
|
|
|
114
143
|
mapping_field = field
|
|
115
144
|
return self.map_by_unique_field(self.query_models(wrapper_type, field, value_list, page_limit), mapping_field)
|
|
116
145
|
|
|
117
|
-
def query_models_with_criteria(self, wrapper_type: type[WrappedType], field:
|
|
146
|
+
def query_models_with_criteria(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
|
|
147
|
+
value_list: Iterable[FieldValue],
|
|
118
148
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
119
149
|
page_limit: int | None = None) \
|
|
120
150
|
-> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
|
|
@@ -131,6 +161,7 @@ class RecordHandler:
|
|
|
131
161
|
:return: The record models for the queried records and the final paging criteria.
|
|
132
162
|
"""
|
|
133
163
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
164
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
134
165
|
pager = QueryDataRecordsAutoPager(dt, field, list(value_list), self.user, paging_criteria)
|
|
135
166
|
pager.max_page = page_limit
|
|
136
167
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
@@ -168,6 +199,19 @@ class RecordHandler:
|
|
|
168
199
|
pager.max_page = page_limit
|
|
169
200
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
170
201
|
|
|
202
|
+
def query_models_by_id_and_map(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
203
|
+
page_limit: int | None = None) -> dict[int, WrappedType]:
|
|
204
|
+
"""
|
|
205
|
+
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
206
|
+
and then converting the results into a dictionary of record ID to the record model for that ID.
|
|
207
|
+
|
|
208
|
+
:param wrapper_type: The record model wrapper to use.
|
|
209
|
+
:param ids: The list of record IDs to query.
|
|
210
|
+
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
211
|
+
:return: The record models for the queried records mapped in a dictionary by their record ID.
|
|
212
|
+
"""
|
|
213
|
+
return {x.record_id: x for x in self.query_models_by_id(wrapper_type, ids, page_limit)}
|
|
214
|
+
|
|
171
215
|
def query_all_models(self, wrapper_type: type[WrappedType], page_limit: int | None = None) -> list[WrappedType]:
|
|
172
216
|
"""
|
|
173
217
|
Shorthand for using the data record manager to query for all data records of a given type
|
|
@@ -200,7 +244,7 @@ class RecordHandler:
|
|
|
200
244
|
|
|
201
245
|
def query_models_by_report(self, wrapper_type: type[WrappedType],
|
|
202
246
|
report_name: str | RawReportTerm | CustomReportCriteria,
|
|
203
|
-
filters: dict[
|
|
247
|
+
filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
|
|
204
248
|
page_limit: int | None = None,
|
|
205
249
|
page_size: int | None = None,
|
|
206
250
|
page_number: int | None = None) -> list[WrappedType]:
|
|
@@ -228,11 +272,11 @@ class RecordHandler:
|
|
|
228
272
|
:return: The record models for the queried records that matched the given report.
|
|
229
273
|
"""
|
|
230
274
|
if isinstance(report_name, str):
|
|
231
|
-
results: list[dict[str,
|
|
232
|
-
|
|
275
|
+
results: list[dict[str, FieldValue]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
|
|
276
|
+
page_limit, page_size, page_number)
|
|
233
277
|
elif isinstance(report_name, RawReportTerm):
|
|
234
|
-
results: list[dict[str,
|
|
235
|
-
|
|
278
|
+
results: list[dict[str, FieldValue]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
|
|
279
|
+
page_limit, page_size, page_number)
|
|
236
280
|
elif isinstance(report_name, CustomReportCriteria):
|
|
237
281
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
238
282
|
# Ensure that the root data type is the one we're looking for.
|
|
@@ -243,8 +287,8 @@ class RecordHandler:
|
|
|
243
287
|
# Enforce that the given custom report has a record ID column.
|
|
244
288
|
if not any([x.data_type_name == dt and x.data_field_name == "RecordId" for x in report_name.column_list]):
|
|
245
289
|
report_name.column_list.append(ReportColumn(dt, "RecordId", FieldType.LONG))
|
|
246
|
-
results: list[dict[str,
|
|
247
|
-
|
|
290
|
+
results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, report_name, filters,
|
|
291
|
+
page_limit, page_size, page_number)
|
|
248
292
|
else:
|
|
249
293
|
raise SapioException("Unrecognized report object.")
|
|
250
294
|
|
|
@@ -273,7 +317,8 @@ class RecordHandler:
|
|
|
273
317
|
"""
|
|
274
318
|
return self.inst_man.add_new_records_of_type(num, wrapper_type)
|
|
275
319
|
|
|
276
|
-
def add_models_with_data(self, wrapper_type: type[WrappedType], fields: list[
|
|
320
|
+
def add_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldIdentifierMap]) \
|
|
321
|
+
-> list[WrappedType]:
|
|
277
322
|
"""
|
|
278
323
|
Shorthand for using the instance manager to add new models of the given type, and then initializing all those
|
|
279
324
|
models with the given fields.
|
|
@@ -283,13 +328,14 @@ class RecordHandler:
|
|
|
283
328
|
:return: The newly added record models with the provided fields set. The records will be in the same order as
|
|
284
329
|
the fields in the fields list.
|
|
285
330
|
"""
|
|
331
|
+
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
286
332
|
models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
|
|
287
333
|
for model, field_list in zip(models, fields):
|
|
288
334
|
model.set_field_values(field_list)
|
|
289
335
|
return models
|
|
290
336
|
|
|
291
|
-
def find_or_add_model(self, wrapper_type: type[WrappedType], primary_identifier:
|
|
292
|
-
secondary_identifiers:
|
|
337
|
+
def find_or_add_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
|
|
338
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType:
|
|
293
339
|
"""
|
|
294
340
|
Find a unique record that matches the given field values. If no such records exist, add a record model to the
|
|
295
341
|
cache with the identifying fields set to the desired values. This record will be created in the system when
|
|
@@ -312,6 +358,8 @@ class RecordHandler:
|
|
|
312
358
|
if secondary_identifiers is None:
|
|
313
359
|
secondary_identifiers = {}
|
|
314
360
|
|
|
361
|
+
primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
|
|
362
|
+
secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
|
|
315
363
|
unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
|
|
316
364
|
secondary_identifiers)
|
|
317
365
|
# If a unique record matched the identifiers, return it.
|
|
@@ -338,7 +386,7 @@ class RecordHandler:
|
|
|
338
386
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
339
387
|
return self.wrap_models(self.dr_man.add_data_records(dt, num), wrapper_type)
|
|
340
388
|
|
|
341
|
-
def create_models_with_data(self, wrapper_type: type[WrappedType], fields: list[
|
|
389
|
+
def create_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldIdentifierMap]) \
|
|
342
390
|
-> list[WrappedType]:
|
|
343
391
|
"""
|
|
344
392
|
Shorthand for creating new records via the data record manager with field data to initialize the records with
|
|
@@ -352,10 +400,12 @@ class RecordHandler:
|
|
|
352
400
|
:return: The newly created record models.
|
|
353
401
|
"""
|
|
354
402
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
403
|
+
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
355
404
|
return self.wrap_models(self.dr_man.add_data_records_with_data(dt, fields), wrapper_type)
|
|
356
405
|
|
|
357
|
-
def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier:
|
|
358
|
-
secondary_identifiers:
|
|
406
|
+
def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
|
|
407
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
|
|
408
|
+
-> WrappedType:
|
|
359
409
|
"""
|
|
360
410
|
Find a unique record that matches the given field values. If no such records exist, create one with the
|
|
361
411
|
identifying fields set to the desired values. If more than one record with the identifying values exists,
|
|
@@ -379,6 +429,8 @@ class RecordHandler:
|
|
|
379
429
|
if secondary_identifiers is None:
|
|
380
430
|
secondary_identifiers = {}
|
|
381
431
|
|
|
432
|
+
primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
|
|
433
|
+
secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
|
|
382
434
|
unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
|
|
383
435
|
secondary_identifiers)
|
|
384
436
|
# If a unique record matched the identifiers, return it.
|
|
@@ -497,7 +549,7 @@ class RecordHandler:
|
|
|
497
549
|
|
|
498
550
|
@staticmethod
|
|
499
551
|
def map_by_child(models: Iterable[RecordModel], child_type: type[WrappedType]) \
|
|
500
|
-
-> dict[WrappedType,
|
|
552
|
+
-> dict[WrappedType, RecordModel]:
|
|
501
553
|
"""
|
|
502
554
|
Take a list of record models and map them by their children. Essentially an inversion of map_to_child.
|
|
503
555
|
If two records share the same child, an exception will be thrown. The children must already be loaded.
|
|
@@ -538,7 +590,7 @@ class RecordHandler:
|
|
|
538
590
|
return by_children
|
|
539
591
|
|
|
540
592
|
@staticmethod
|
|
541
|
-
def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name:
|
|
593
|
+
def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
542
594
|
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
|
|
543
595
|
"""
|
|
544
596
|
Map a list of record models to their forward side link. The forward side link must already be loaded.
|
|
@@ -549,13 +601,14 @@ class RecordHandler:
|
|
|
549
601
|
:return: A dict[ModelType, SlideLink]. If an input model doesn't have a forward side link of the given type,
|
|
550
602
|
then it will map to None.
|
|
551
603
|
"""
|
|
604
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
552
605
|
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
553
606
|
for model in models:
|
|
554
607
|
return_dict[model] = model.get_forward_side_link(field_name, side_link_type)
|
|
555
608
|
return return_dict
|
|
556
609
|
|
|
557
610
|
@staticmethod
|
|
558
|
-
def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name:
|
|
611
|
+
def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
559
612
|
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
560
613
|
"""
|
|
561
614
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
@@ -568,6 +621,7 @@ class RecordHandler:
|
|
|
568
621
|
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
|
|
569
622
|
pointing to it, then it will not be in the resulting dictionary.
|
|
570
623
|
"""
|
|
624
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
571
625
|
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
572
626
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
573
627
|
by_side_link: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
@@ -578,7 +632,7 @@ class RecordHandler:
|
|
|
578
632
|
return by_side_link
|
|
579
633
|
|
|
580
634
|
@staticmethod
|
|
581
|
-
def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name:
|
|
635
|
+
def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
582
636
|
side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
|
|
583
637
|
"""
|
|
584
638
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
@@ -591,6 +645,7 @@ class RecordHandler:
|
|
|
591
645
|
:return: A dict[SideLink, ModelType]. If an input model doesn't have a forward side link of the given type
|
|
592
646
|
pointing to it, then it will not be in the resulting dictionary.
|
|
593
647
|
"""
|
|
648
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
594
649
|
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
595
650
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
596
651
|
by_side_link: dict[WrappedType, WrappedRecordModel] = {}
|
|
@@ -604,7 +659,7 @@ class RecordHandler:
|
|
|
604
659
|
return by_side_link
|
|
605
660
|
|
|
606
661
|
@staticmethod
|
|
607
|
-
def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name:
|
|
662
|
+
def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
608
663
|
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, list[WrappedType]]:
|
|
609
664
|
"""
|
|
610
665
|
Map a list of record models to a list reverse side links of a given type. The reverse side links must already
|
|
@@ -617,13 +672,14 @@ class RecordHandler:
|
|
|
617
672
|
:return: A dict[ModelType, list[SideLink]]. If an input model doesn't have reverse side links of the given type,
|
|
618
673
|
then it will map to an empty list.
|
|
619
674
|
"""
|
|
675
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
620
676
|
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
621
677
|
for model in models:
|
|
622
678
|
return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
|
|
623
679
|
return return_dict
|
|
624
680
|
|
|
625
681
|
@staticmethod
|
|
626
|
-
def map_to_reverse_side_link(models: Iterable[WrappedRecordModel], field_name:
|
|
682
|
+
def map_to_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
627
683
|
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
|
|
628
684
|
"""
|
|
629
685
|
Map a list of record models to the reverse side link of a given type. If a given record has more than one
|
|
@@ -636,6 +692,7 @@ class RecordHandler:
|
|
|
636
692
|
:return: A dict[ModelType, SideLink]. If an input model doesn't have reverse side links of the given type,
|
|
637
693
|
then it will map to None.
|
|
638
694
|
"""
|
|
695
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
639
696
|
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
640
697
|
for model in models:
|
|
641
698
|
links: list[WrappedType] = model.get_reverse_side_link(field_name, side_link_type)
|
|
@@ -646,7 +703,7 @@ class RecordHandler:
|
|
|
646
703
|
return return_dict
|
|
647
704
|
|
|
648
705
|
@staticmethod
|
|
649
|
-
def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name:
|
|
706
|
+
def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
650
707
|
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
651
708
|
"""
|
|
652
709
|
Take a list of record models and map them by their reverse side links. Essentially an inversion of
|
|
@@ -660,6 +717,7 @@ class RecordHandler:
|
|
|
660
717
|
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
|
|
661
718
|
pointing to it, then it will not be in the resulting dictionary.
|
|
662
719
|
"""
|
|
720
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
663
721
|
to_side_links: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler\
|
|
664
722
|
.map_to_reverse_side_links(models, field_name, side_link_type)
|
|
665
723
|
by_side_links: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
@@ -669,7 +727,7 @@ class RecordHandler:
|
|
|
669
727
|
return by_side_links
|
|
670
728
|
|
|
671
729
|
@staticmethod
|
|
672
|
-
def map_by_reverse_side_link(models: Iterable[WrappedRecordModel], field_name:
|
|
730
|
+
def map_by_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
673
731
|
side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
|
|
674
732
|
"""
|
|
675
733
|
Take a list of record models and map them by their reverse side link. Essentially an inversion of
|
|
@@ -683,6 +741,7 @@ class RecordHandler:
|
|
|
683
741
|
:return: A dict[SideLink, ModelType]. If an input model doesn't have a reverse side link of the given type
|
|
684
742
|
pointing to it, then it will not be in the resulting dictionary.
|
|
685
743
|
"""
|
|
744
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
686
745
|
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
687
746
|
.map_to_reverse_side_link(models, field_name, side_link_type)
|
|
688
747
|
by_side_link: dict[WrappedType, WrappedRecordModel] = {}
|
|
@@ -709,7 +768,8 @@ class RecordHandler:
|
|
|
709
768
|
return ret_dict
|
|
710
769
|
|
|
711
770
|
@staticmethod
|
|
712
|
-
def map_by_field(models: Iterable[SapioRecord], field_name:
|
|
771
|
+
def map_by_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
|
|
772
|
+
-> dict[FieldValue, list[SapioRecord]]:
|
|
713
773
|
"""
|
|
714
774
|
Map the given records by one of their fields. If any two records share the same field value, they'll appear in
|
|
715
775
|
the same value list.
|
|
@@ -718,14 +778,16 @@ class RecordHandler:
|
|
|
718
778
|
:param field_name: The field name to map against.
|
|
719
779
|
:return: A dict mapping field values to the records with that value.
|
|
720
780
|
"""
|
|
721
|
-
|
|
781
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
782
|
+
ret_dict: dict[FieldValue, list[SapioRecord]] = {}
|
|
722
783
|
for model in models:
|
|
723
|
-
val:
|
|
784
|
+
val: FieldValue = model.get_field_value(field_name)
|
|
724
785
|
ret_dict.setdefault(val, []).append(model)
|
|
725
786
|
return ret_dict
|
|
726
787
|
|
|
727
788
|
@staticmethod
|
|
728
|
-
def map_by_unique_field(models: Iterable[SapioRecord], field_name:
|
|
789
|
+
def map_by_unique_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
|
|
790
|
+
-> dict[FieldValue, SapioRecord]:
|
|
729
791
|
"""
|
|
730
792
|
Uniquely map the given records by one of their fields. If any two records share the same field value, throws
|
|
731
793
|
an exception.
|
|
@@ -734,16 +796,17 @@ class RecordHandler:
|
|
|
734
796
|
:param field_name: The field name to map against.
|
|
735
797
|
:return: A dict mapping field values to the record with that value.
|
|
736
798
|
"""
|
|
737
|
-
|
|
799
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
800
|
+
ret_dict: dict[FieldValue, SapioRecord] = {}
|
|
738
801
|
for model in models:
|
|
739
|
-
val:
|
|
802
|
+
val: FieldValue = model.get_field_value(field_name)
|
|
740
803
|
if val in ret_dict:
|
|
741
804
|
raise SapioException(f"Value {val} encountered more than once in models list.")
|
|
742
805
|
ret_dict.update({val: model})
|
|
743
806
|
return ret_dict
|
|
744
807
|
|
|
745
808
|
@staticmethod
|
|
746
|
-
def sum_of_field(models: Iterable[SapioRecord], field_name:
|
|
809
|
+
def sum_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
747
810
|
"""
|
|
748
811
|
Sum up the numeric value of a given field across all input models. Excepts that all given models have a value.
|
|
749
812
|
If the field is an integer field, the value will be converted to a float.
|
|
@@ -752,13 +815,14 @@ class RecordHandler:
|
|
|
752
815
|
:param field_name: The name of the numeric field to sum.
|
|
753
816
|
:return: The sum of the field values for the collection of models.
|
|
754
817
|
"""
|
|
818
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
755
819
|
field_sum: float = 0
|
|
756
820
|
for model in models:
|
|
757
821
|
field_sum += float(model.get_field_value(field_name))
|
|
758
822
|
return field_sum
|
|
759
823
|
|
|
760
824
|
@staticmethod
|
|
761
|
-
def mean_of_field(models: Iterable[SapioRecord], field_name:
|
|
825
|
+
def mean_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
762
826
|
"""
|
|
763
827
|
Calculate the mean of the numeric value of a given field across all input models. Excepts that all given models
|
|
764
828
|
have a value. If the field is an integer field, the value will be converted to a float.
|
|
@@ -799,8 +863,8 @@ class RecordHandler:
|
|
|
799
863
|
return oldest
|
|
800
864
|
|
|
801
865
|
@staticmethod
|
|
802
|
-
def values_to_field_maps(field_name:
|
|
803
|
-
|
|
866
|
+
def values_to_field_maps(field_name: FieldIdentifier, values: Iterable[FieldValue],
|
|
867
|
+
existing_fields: list[FieldIdentifier] | None = None) -> list[FieldMap]:
|
|
804
868
|
"""
|
|
805
869
|
Add a list of values for a specific field to a list of dictionaries pairing each value to that field name.
|
|
806
870
|
|
|
@@ -811,6 +875,8 @@ class RecordHandler:
|
|
|
811
875
|
:return: A fields map list that contains the given values mapped by the given field name.
|
|
812
876
|
"""
|
|
813
877
|
# Update the existing fields map list if one is given.
|
|
878
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
879
|
+
existing_fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(existing_fields)
|
|
814
880
|
if existing_fields:
|
|
815
881
|
values = list(values)
|
|
816
882
|
# The number of new values must match the length of the existing fields list.
|
|
@@ -831,8 +897,6 @@ class RecordHandler:
|
|
|
831
897
|
path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
|
|
832
898
|
relationship path must already be loaded.
|
|
833
899
|
|
|
834
|
-
Currently, the relationship path may only contain parent/child nodes.
|
|
835
|
-
|
|
836
900
|
:param models: A list of record models.
|
|
837
901
|
:param path: The relationship path to follow.
|
|
838
902
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -843,15 +907,44 @@ class RecordHandler:
|
|
|
843
907
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
844
908
|
path: list[RelationshipNode] = path.path
|
|
845
909
|
for model in models:
|
|
846
|
-
current: PyRecordModel = model if isinstance(model, PyRecordModel) else model.backing_model
|
|
910
|
+
current: PyRecordModel | None = model if isinstance(model, PyRecordModel) else model.backing_model
|
|
847
911
|
for node in path:
|
|
848
|
-
|
|
912
|
+
data_type: str = node.data_type_name
|
|
913
|
+
direction: RelationshipNodeType = node.direction
|
|
849
914
|
if current is None:
|
|
850
915
|
break
|
|
851
916
|
if direction == RelationshipNodeType.CHILD:
|
|
852
|
-
current = current.get_child_of_type(
|
|
917
|
+
current = current.get_child_of_type(data_type)
|
|
853
918
|
elif direction == RelationshipNodeType.PARENT:
|
|
854
|
-
current = current.get_parent_of_type(
|
|
919
|
+
current = current.get_parent_of_type(data_type)
|
|
920
|
+
elif direction == RelationshipNodeType.ANCESTOR:
|
|
921
|
+
ancestors: list[PyRecordModel] = list(self.an_man.get_ancestors_of_type(current, data_type))
|
|
922
|
+
if not ancestors:
|
|
923
|
+
current = None
|
|
924
|
+
elif len(ancestors) > 1:
|
|
925
|
+
raise SapioException(f"Hierarchy contains multiple ancestors of type {data_type}.")
|
|
926
|
+
else:
|
|
927
|
+
current = ancestors[0]
|
|
928
|
+
elif direction == RelationshipNodeType.DESCENDANT:
|
|
929
|
+
descendants: list[PyRecordModel] = list(self.an_man.get_descendant_of_type(current, data_type))
|
|
930
|
+
if not descendants:
|
|
931
|
+
current = None
|
|
932
|
+
elif len(descendants) > 1:
|
|
933
|
+
raise SapioException(f"Hierarchy contains multiple descendants of type {data_type}.")
|
|
934
|
+
else:
|
|
935
|
+
current = descendants[0]
|
|
936
|
+
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
937
|
+
current = current.get_forward_side_link(node.data_field_name)
|
|
938
|
+
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
939
|
+
field_name: str = node.data_field_name
|
|
940
|
+
reverse_links: list[PyRecordModel] = current.get_reverse_side_link(field_name, data_type)
|
|
941
|
+
if not reverse_links:
|
|
942
|
+
current = None
|
|
943
|
+
elif len(reverse_links) > 1:
|
|
944
|
+
raise SapioException(f"Hierarchy contains multiple reverse links of type {data_type} on field "
|
|
945
|
+
f"{field_name}.")
|
|
946
|
+
else:
|
|
947
|
+
current = reverse_links[0]
|
|
855
948
|
else:
|
|
856
949
|
raise SapioException("Unsupported path direction.")
|
|
857
950
|
ret_dict.update({model: self.inst_man.wrap(current, wrapper_type) if current else None})
|
|
@@ -864,8 +957,6 @@ class RecordHandler:
|
|
|
864
957
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
865
958
|
relationship path must already be loaded.
|
|
866
959
|
|
|
867
|
-
Currently, the relationship path may only contain parent/child nodes.
|
|
868
|
-
|
|
869
960
|
:param models: A list of record models.
|
|
870
961
|
:param path: The relationship path to follow.
|
|
871
962
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -880,14 +971,23 @@ class RecordHandler:
|
|
|
880
971
|
next_search: set[PyRecordModel] = set()
|
|
881
972
|
# Exhaust the records at each step in the path, then use those records for the next step.
|
|
882
973
|
for node in path:
|
|
883
|
-
|
|
974
|
+
data_type: str = node.data_type_name
|
|
975
|
+
direction: RelationshipNodeType = node.direction
|
|
884
976
|
if len(current_search) == 0:
|
|
885
977
|
break
|
|
886
978
|
for search in current_search:
|
|
887
979
|
if direction == RelationshipNodeType.CHILD:
|
|
888
|
-
next_search.update(search.get_children_of_type(
|
|
980
|
+
next_search.update(search.get_children_of_type(data_type))
|
|
889
981
|
elif direction == RelationshipNodeType.PARENT:
|
|
890
|
-
next_search.update(search.get_parents_of_type(
|
|
982
|
+
next_search.update(search.get_parents_of_type(data_type))
|
|
983
|
+
elif direction == RelationshipNodeType.ANCESTOR:
|
|
984
|
+
next_search.update(self.an_man.get_ancestors_of_type(search, data_type))
|
|
985
|
+
elif direction == RelationshipNodeType.DESCENDANT:
|
|
986
|
+
next_search.update(self.an_man.get_descendant_of_type(search, data_type))
|
|
987
|
+
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
988
|
+
next_search.add(search.get_forward_side_link(node.data_field_name))
|
|
989
|
+
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
990
|
+
next_search.update(search.get_reverse_side_link(node.data_field_name, data_type))
|
|
891
991
|
else:
|
|
892
992
|
raise SapioException("Unsupported path direction.")
|
|
893
993
|
current_search = next_search
|
|
@@ -908,8 +1008,6 @@ class RecordHandler:
|
|
|
908
1008
|
relationships (e.g. a sample which is aliquoted to a number of samples, then those aliquots are pooled back
|
|
909
1009
|
together into a single sample).
|
|
910
1010
|
|
|
911
|
-
Currently, the relationship path may only contain parent/child nodes.
|
|
912
|
-
|
|
913
1011
|
:param models: A list of record models.
|
|
914
1012
|
:param path: The relationship path to follow.
|
|
915
1013
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -922,20 +1020,29 @@ class RecordHandler:
|
|
|
922
1020
|
for model in models:
|
|
923
1021
|
current: list[PyRecordModel] = [model if isinstance(model, PyRecordModel) else model.backing_model]
|
|
924
1022
|
for node in path:
|
|
925
|
-
|
|
1023
|
+
data_type: str = node.data_type_name
|
|
1024
|
+
direction: RelationshipNodeType = node.direction
|
|
926
1025
|
if len(current) == 0:
|
|
927
1026
|
break
|
|
928
1027
|
if direction == RelationshipNodeType.CHILD:
|
|
929
|
-
current = current[0].get_children_of_type(
|
|
1028
|
+
current = current[0].get_children_of_type(data_type)
|
|
930
1029
|
elif direction == RelationshipNodeType.PARENT:
|
|
931
|
-
current = current[0].get_parents_of_type(
|
|
1030
|
+
current = current[0].get_parents_of_type(data_type)
|
|
1031
|
+
elif direction == RelationshipNodeType.ANCESTOR:
|
|
1032
|
+
current = list(self.an_man.get_ancestors_of_type(current[0], data_type))
|
|
1033
|
+
elif direction == RelationshipNodeType.DESCENDANT:
|
|
1034
|
+
current = list(self.an_man.get_descendant_of_type(current[0], data_type))
|
|
1035
|
+
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
1036
|
+
current = [current[0].get_forward_side_link(node.data_field_name)]
|
|
1037
|
+
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
1038
|
+
current = current[0].get_reverse_side_link(node.data_field_name, data_type)
|
|
932
1039
|
else:
|
|
933
1040
|
raise SapioException("Unsupported path direction.")
|
|
934
1041
|
ret_dict.update({model: self.inst_man.wrap(current[0], wrapper_type) if current else None})
|
|
935
1042
|
return ret_dict
|
|
936
1043
|
|
|
937
|
-
def __find_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value:
|
|
938
|
-
secondary_identifiers:
|
|
1044
|
+
def __find_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value: FieldValue,
|
|
1045
|
+
secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType | None:
|
|
939
1046
|
"""
|
|
940
1047
|
Find a record from the system that matches the given field values. The primary identifier and value is used
|
|
941
1048
|
to query for the record, then the secondary identifiers may be optionally provided to further filter the
|
|
@@ -959,3 +1066,18 @@ class RecordHandler:
|
|
|
959
1066
|
f"encountered in system that matches all provided identifiers.")
|
|
960
1067
|
unique_record = result
|
|
961
1068
|
return unique_record
|
|
1069
|
+
|
|
1070
|
+
@staticmethod
|
|
1071
|
+
def __verify_data_type(records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> None:
|
|
1072
|
+
"""
|
|
1073
|
+
Throw an exception if the data type of the given records and wrapper don't match.
|
|
1074
|
+
"""
|
|
1075
|
+
model_type: str = wrapper_type.get_wrapper_data_type_name()
|
|
1076
|
+
for record in records:
|
|
1077
|
+
record_type: str = record.data_type_name
|
|
1078
|
+
# Account for ELN data type records.
|
|
1079
|
+
if ElnBaseDataType.is_eln_type(record_type):
|
|
1080
|
+
record_type = ElnBaseDataType.get_base_type(record_type).data_type_name
|
|
1081
|
+
if record_type != model_type:
|
|
1082
|
+
raise SapioException(f"Data record of type {record_type} cannot be wrapped by the record model wrapper "
|
|
1083
|
+
f"of type {model_type}")
|