sapiopycommons 2024.11.11a364__py3-none-any.whl → 2024.11.18a366__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 +532 -83
- sapiopycommons/callbacks/field_builder.py +537 -0
- sapiopycommons/chem/IndigoMolecules.py +2 -0
- sapiopycommons/chem/Molecules.py +77 -18
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +60 -0
- sapiopycommons/customreport/custom_report_builder.py +130 -0
- sapiopycommons/customreport/term_builder.py +299 -0
- sapiopycommons/datatype/attachment_util.py +11 -10
- sapiopycommons/datatype/data_fields.py +61 -0
- sapiopycommons/datatype/pseudo_data_types.py +440 -0
- sapiopycommons/eln/experiment_handler.py +272 -70
- sapiopycommons/eln/experiment_report_util.py +653 -0
- sapiopycommons/files/complex_data_loader.py +5 -4
- sapiopycommons/files/file_bridge.py +31 -24
- sapiopycommons/files/file_bridge_handler.py +340 -0
- sapiopycommons/files/file_data_handler.py +2 -5
- sapiopycommons/files/file_util.py +59 -9
- sapiopycommons/files/file_validator.py +92 -6
- sapiopycommons/files/file_writer.py +44 -15
- sapiopycommons/flowcyto/flow_cyto.py +77 -0
- sapiopycommons/flowcyto/flowcyto_data.py +75 -0
- sapiopycommons/general/accession_service.py +375 -0
- sapiopycommons/general/aliases.py +207 -6
- sapiopycommons/general/audit_log.py +189 -0
- sapiopycommons/general/custom_report_util.py +212 -37
- sapiopycommons/general/exceptions.py +21 -8
- sapiopycommons/general/popup_util.py +21 -0
- sapiopycommons/general/sapio_links.py +50 -0
- sapiopycommons/general/time_util.py +8 -2
- sapiopycommons/multimodal/multimodal.py +146 -0
- sapiopycommons/multimodal/multimodal_data.py +490 -0
- sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +481 -97
- sapiopycommons/rules/eln_rule_handler.py +34 -25
- sapiopycommons/rules/on_save_rule_handler.py +34 -31
- sapiopycommons/sftpconnect/__init__.py +0 -0
- sapiopycommons/sftpconnect/sftp_builder.py +69 -0
- sapiopycommons/webhook/webhook_context.py +39 -0
- sapiopycommons/webhook/webhook_handlers.py +201 -42
- sapiopycommons/webhook/webservice_handlers.py +67 -0
- {sapiopycommons-2024.11.11a364.dist-info → sapiopycommons-2024.11.18a366.dist-info}/METADATA +5 -2
- sapiopycommons-2024.11.18a366.dist-info/RECORD +59 -0
- {sapiopycommons-2024.11.11a364.dist-info → sapiopycommons-2024.11.18a366.dist-info}/WHEEL +1 -1
- sapiopycommons-2024.11.11a364.dist-info/RECORD +0 -38
- {sapiopycommons-2024.11.11a364.dist-info → sapiopycommons-2024.11.18a366.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,11 +1,15 @@
|
|
|
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
|
|
8
|
+
from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, RawReportTerm, ReportColumn
|
|
6
9
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
7
10
|
from sapiopylib.rest.pojo.DataRecordPaging import DataRecordPojoPageCriteria
|
|
8
|
-
from sapiopylib.rest.pojo.
|
|
11
|
+
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
12
|
+
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
9
13
|
from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
|
|
10
14
|
QueryAllRecordsOfTypeAutoPager
|
|
11
15
|
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
@@ -14,8 +18,10 @@ from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelMana
|
|
|
14
18
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, WrappedRecordModel
|
|
15
19
|
from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
|
|
16
20
|
RelationshipNodeType
|
|
21
|
+
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
17
22
|
|
|
18
|
-
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap
|
|
23
|
+
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
|
|
24
|
+
FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey
|
|
19
25
|
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
20
26
|
from sapiopycommons.general.exceptions import SapioException
|
|
21
27
|
|
|
@@ -30,16 +36,38 @@ class RecordHandler:
|
|
|
30
36
|
rec_man: RecordModelManager
|
|
31
37
|
inst_man: RecordModelInstanceManager
|
|
32
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
|
|
33
55
|
|
|
34
|
-
def __init__(self, context:
|
|
56
|
+
def __init__(self, context: UserIdentifier):
|
|
35
57
|
"""
|
|
36
58
|
:param context: The current webhook context or a user object to send requests from.
|
|
37
59
|
"""
|
|
60
|
+
self.user = AliasUtil.to_sapio_user(context)
|
|
61
|
+
if self.__initialized:
|
|
62
|
+
return
|
|
63
|
+
self.__initialized = True
|
|
64
|
+
|
|
38
65
|
self.user = context if isinstance(context, SapioUser) else context.user
|
|
39
66
|
self.dr_man = DataRecordManager(self.user)
|
|
40
67
|
self.rec_man = RecordModelManager(self.user)
|
|
41
68
|
self.inst_man = self.rec_man.instance_manager
|
|
42
69
|
self.rel_man = self.rec_man.relationship_manager
|
|
70
|
+
self.an_man = RecordModelAncestorManager(self.rec_man)
|
|
43
71
|
|
|
44
72
|
def wrap_model(self, record: DataRecord, wrapper_type: type[WrappedType]) -> WrappedType:
|
|
45
73
|
"""
|
|
@@ -49,6 +77,7 @@ class RecordHandler:
|
|
|
49
77
|
:param wrapper_type: The record model wrapper to use.
|
|
50
78
|
:return: The record model for the input.
|
|
51
79
|
"""
|
|
80
|
+
self.__verify_data_type([record], wrapper_type)
|
|
52
81
|
return self.inst_man.add_existing_record_of_type(record, wrapper_type)
|
|
53
82
|
|
|
54
83
|
def wrap_models(self, records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> list[WrappedType]:
|
|
@@ -59,10 +88,11 @@ class RecordHandler:
|
|
|
59
88
|
:param wrapper_type: The record model wrapper to use.
|
|
60
89
|
:return: The record models for the input.
|
|
61
90
|
"""
|
|
91
|
+
self.__verify_data_type(records, wrapper_type)
|
|
62
92
|
return self.inst_man.add_existing_records_of_type(list(records), wrapper_type)
|
|
63
93
|
|
|
64
|
-
def query_models(self, wrapper_type: type[WrappedType], field:
|
|
65
|
-
page_limit: int | None = None) -> list[WrappedType]:
|
|
94
|
+
def query_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier, value_list: Iterable[FieldValue],
|
|
95
|
+
page_limit: int | None = None, page_size: int | None = None) -> list[WrappedType]:
|
|
66
96
|
"""
|
|
67
97
|
Shorthand for using the data record manager to query for a list of data records by field value
|
|
68
98
|
and then converting the results into a list of record models.
|
|
@@ -70,12 +100,63 @@ class RecordHandler:
|
|
|
70
100
|
:param wrapper_type: The record model wrapper to use.
|
|
71
101
|
:param field: The field to query on.
|
|
72
102
|
:param value_list: The values of the field to query on.
|
|
73
|
-
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
103
|
+
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
104
|
+
only functions if you set a page size or the platform enforces a page size.
|
|
105
|
+
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
74
106
|
:return: The record models for the queried records.
|
|
75
107
|
"""
|
|
76
|
-
|
|
108
|
+
criteria: DataRecordPojoPageCriteria | None = None
|
|
109
|
+
if page_size is not None:
|
|
110
|
+
criteria = DataRecordPojoPageCriteria(page_size=page_size)
|
|
111
|
+
return self.query_models_with_criteria(wrapper_type, field, value_list, criteria, page_limit)[0]
|
|
77
112
|
|
|
78
|
-
def
|
|
113
|
+
def query_and_map_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
|
|
114
|
+
value_list: Iterable[FieldValue], page_limit: int | None = None,
|
|
115
|
+
page_size: int | None = None, *, mapping_field: FieldIdentifier | None = None) \
|
|
116
|
+
-> dict[FieldValue, list[WrappedType]]:
|
|
117
|
+
"""
|
|
118
|
+
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
119
|
+
map_by_field to turn the returned list into a dictionary mapping field values to records.
|
|
120
|
+
|
|
121
|
+
:param wrapper_type: The record model wrapper to use.
|
|
122
|
+
:param field: The field to query and map on.
|
|
123
|
+
:param value_list: The values of the field to query on.
|
|
124
|
+
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
125
|
+
only functions if you set a page size or the platform enforces a page size.
|
|
126
|
+
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
127
|
+
:param mapping_field: If provided, use this field to map against instead of the field that was queried on.
|
|
128
|
+
:return: The record models for the queried records mapped by field values to the records with that value.
|
|
129
|
+
"""
|
|
130
|
+
if mapping_field is None:
|
|
131
|
+
mapping_field = field
|
|
132
|
+
return self.map_by_field(self.query_models(wrapper_type, field, value_list, page_limit, page_size),
|
|
133
|
+
mapping_field)
|
|
134
|
+
|
|
135
|
+
def query_and_unique_map_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
|
|
136
|
+
value_list: Iterable[FieldValue], page_limit: int | None = None,
|
|
137
|
+
page_size: int | None = None, *, mapping_field: FieldIdentifier | None = None) \
|
|
138
|
+
-> dict[FieldValue, WrappedType]:
|
|
139
|
+
"""
|
|
140
|
+
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
141
|
+
map_by_unique_field to turn the returned list into a dictionary mapping field values to records.
|
|
142
|
+
If any two records share the same field value, throws an exception.
|
|
143
|
+
|
|
144
|
+
:param wrapper_type: The record model wrapper to use.
|
|
145
|
+
:param field: The field to query and map on.
|
|
146
|
+
:param value_list: The values of the field to query on.
|
|
147
|
+
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
148
|
+
only functions if you set a page size or the platform enforces a page size.
|
|
149
|
+
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
150
|
+
:param mapping_field: If provided, use this field to map against instead of the field that was queried on.
|
|
151
|
+
:return: The record models for the queried records mapped by field values to the record with that value.
|
|
152
|
+
"""
|
|
153
|
+
if mapping_field is None:
|
|
154
|
+
mapping_field = field
|
|
155
|
+
return self.map_by_unique_field(self.query_models(wrapper_type, field, value_list, page_limit, page_size),
|
|
156
|
+
mapping_field)
|
|
157
|
+
|
|
158
|
+
def query_models_with_criteria(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
|
|
159
|
+
value_list: Iterable[FieldValue],
|
|
79
160
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
80
161
|
page_limit: int | None = None) \
|
|
81
162
|
-> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
|
|
@@ -88,26 +169,33 @@ class RecordHandler:
|
|
|
88
169
|
:param value_list: The values of the field to query on.
|
|
89
170
|
:param paging_criteria: The paging criteria to start the query with.
|
|
90
171
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
91
|
-
possible pages.
|
|
172
|
+
possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
|
|
173
|
+
enforces a page size.
|
|
92
174
|
:return: The record models for the queried records and the final paging criteria.
|
|
93
175
|
"""
|
|
94
176
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
177
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
95
178
|
pager = QueryDataRecordsAutoPager(dt, field, list(value_list), self.user, paging_criteria)
|
|
96
179
|
pager.max_page = page_limit
|
|
97
180
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
98
181
|
|
|
99
182
|
def query_models_by_id(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
100
|
-
page_limit: int | None = None) -> list[WrappedType]:
|
|
183
|
+
page_limit: int | None = None, page_size: int | None = None) -> list[WrappedType]:
|
|
101
184
|
"""
|
|
102
185
|
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
103
186
|
and then converting the results into a list of record models.
|
|
104
187
|
|
|
105
188
|
:param wrapper_type: The record model wrapper to use.
|
|
106
189
|
:param ids: The list of record IDs to query.
|
|
107
|
-
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
190
|
+
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
191
|
+
only functions if you set a page size or the platform enforces a page size.
|
|
192
|
+
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
108
193
|
:return: The record models for the queried records.
|
|
109
194
|
"""
|
|
110
|
-
|
|
195
|
+
criteria: DataRecordPojoPageCriteria | None = None
|
|
196
|
+
if page_size is not None:
|
|
197
|
+
criteria = DataRecordPojoPageCriteria(page_size=page_size)
|
|
198
|
+
return self.query_models_by_id_with_criteria(wrapper_type, ids, criteria, page_limit)[0]
|
|
111
199
|
|
|
112
200
|
def query_models_by_id_with_criteria(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
113
201
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
@@ -121,7 +209,8 @@ class RecordHandler:
|
|
|
121
209
|
:param ids: The list of record IDs to query.
|
|
122
210
|
:param paging_criteria: The paging criteria to start the query with.
|
|
123
211
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
124
|
-
possible pages.
|
|
212
|
+
possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
|
|
213
|
+
enforces a page size.
|
|
125
214
|
:return: The record models for the queried records and the final paging criteria.
|
|
126
215
|
"""
|
|
127
216
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
@@ -129,16 +218,38 @@ class RecordHandler:
|
|
|
129
218
|
pager.max_page = page_limit
|
|
130
219
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
131
220
|
|
|
132
|
-
def
|
|
221
|
+
def query_models_by_id_and_map(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
222
|
+
page_limit: int | None = None, page_size: int | None = None) \
|
|
223
|
+
-> dict[int, WrappedType]:
|
|
224
|
+
"""
|
|
225
|
+
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
226
|
+
and then converting the results into a dictionary of record ID to the record model for that ID.
|
|
227
|
+
|
|
228
|
+
:param wrapper_type: The record model wrapper to use.
|
|
229
|
+
:param ids: The list of record IDs to query.
|
|
230
|
+
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
231
|
+
only functions if you set a page size or the platform enforces a page size.
|
|
232
|
+
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
233
|
+
:return: The record models for the queried records mapped in a dictionary by their record ID.
|
|
234
|
+
"""
|
|
235
|
+
return {x.record_id: x for x in self.query_models_by_id(wrapper_type, ids, page_limit, page_size)}
|
|
236
|
+
|
|
237
|
+
def query_all_models(self, wrapper_type: type[WrappedType], page_limit: int | None = None,
|
|
238
|
+
page_size: int | None = None) -> list[WrappedType]:
|
|
133
239
|
"""
|
|
134
240
|
Shorthand for using the data record manager to query for all data records of a given type
|
|
135
241
|
and then converting the results into a list of record models.
|
|
136
242
|
|
|
137
243
|
:param wrapper_type: The record model wrapper to use.
|
|
138
|
-
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
244
|
+
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
245
|
+
only functions if you set a page size or the platform enforces a page size.
|
|
246
|
+
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
139
247
|
:return: The record models for the queried records.
|
|
140
248
|
"""
|
|
141
|
-
|
|
249
|
+
criteria: DataRecordPojoPageCriteria | None = None
|
|
250
|
+
if page_size is not None:
|
|
251
|
+
criteria = DataRecordPojoPageCriteria(page_size=page_size)
|
|
252
|
+
return self.query_all_models_with_criteria(wrapper_type, criteria, page_limit)[0]
|
|
142
253
|
|
|
143
254
|
def query_all_models_with_criteria(self, wrapper_type: type[WrappedType],
|
|
144
255
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
@@ -151,7 +262,8 @@ class RecordHandler:
|
|
|
151
262
|
:param wrapper_type: The record model wrapper to use.
|
|
152
263
|
:param paging_criteria: The paging criteria to start the query with.
|
|
153
264
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
154
|
-
possible pages.
|
|
265
|
+
possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
|
|
266
|
+
enforces a page size.
|
|
155
267
|
:return: The record models for the queried records and the final paging criteria.
|
|
156
268
|
"""
|
|
157
269
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
@@ -160,24 +272,58 @@ class RecordHandler:
|
|
|
160
272
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
161
273
|
|
|
162
274
|
def query_models_by_report(self, wrapper_type: type[WrappedType],
|
|
163
|
-
report_name: str,
|
|
164
|
-
filters: dict[
|
|
165
|
-
page_limit: int | None = None
|
|
275
|
+
report_name: str | RawReportTerm | CustomReportCriteria,
|
|
276
|
+
filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
|
|
277
|
+
page_limit: int | None = None,
|
|
278
|
+
page_size: int | None = None,
|
|
279
|
+
page_number: int | None = None) -> list[WrappedType]:
|
|
166
280
|
"""
|
|
167
|
-
Run a
|
|
168
|
-
First runs the
|
|
281
|
+
Run a report and use the results of that report to query for and return the records in the report results.
|
|
282
|
+
First runs the report, then runs a data record manager query on the results of the custom report.
|
|
169
283
|
|
|
170
|
-
Will throw an exception if the
|
|
284
|
+
Will throw an exception if given the name of a system report that does not have a RecordId column.
|
|
285
|
+
Quick and custom reports are guaranteed to have a record ID column.
|
|
286
|
+
|
|
287
|
+
Any given custom report criteria should only have columns from a single data type.
|
|
171
288
|
|
|
172
289
|
:param wrapper_type: The record model wrapper to use.
|
|
173
|
-
:param report_name: The name of
|
|
290
|
+
:param report_name: The name of a system report, or a raw report term for a quick report, or custom report
|
|
291
|
+
criteria for a custom report.
|
|
174
292
|
:param filters: If provided, filter the results of the report using the given mapping of headers to values to
|
|
175
293
|
filter on. This filtering is done before the records are queried.
|
|
176
294
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
177
|
-
:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
295
|
+
:param page_size: The size of each page of results in the search. If None, the page size is set by the server.
|
|
296
|
+
If the input report is a custom report criteria, uses the value from the criteria, unless this value is
|
|
297
|
+
not None, in which case it overwrites the given report's value.
|
|
298
|
+
:param page_number: The page number to start the search from, If None, starts on the first page.
|
|
299
|
+
If the input report is a custom report criteria, uses the value from the criteria, unless this value is
|
|
300
|
+
not None, in which case it overwrites the given report's value.
|
|
301
|
+
:return: The record models for the queried records that matched the given report.
|
|
302
|
+
"""
|
|
303
|
+
if isinstance(report_name, str):
|
|
304
|
+
results: list[dict[str, FieldValue]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
|
|
305
|
+
page_limit, page_size, page_number)
|
|
306
|
+
elif isinstance(report_name, RawReportTerm):
|
|
307
|
+
results: list[dict[str, FieldValue]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
|
|
308
|
+
page_limit, page_size, page_number)
|
|
309
|
+
elif isinstance(report_name, CustomReportCriteria):
|
|
310
|
+
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
311
|
+
# Ensure that the root data type is the one we're looking for.
|
|
312
|
+
report_name.root_data_type = dt
|
|
313
|
+
# Raise an exception if any column in the report doesn't match the given data type.
|
|
314
|
+
if any([x.data_type_name != dt for x in report_name.column_list]):
|
|
315
|
+
raise SapioException("You may only query records from a report containing columns from that data type.")
|
|
316
|
+
# Enforce that the given custom report has a record ID column.
|
|
317
|
+
if not any([x.data_type_name == dt and x.data_field_name == "RecordId" for x in report_name.column_list]):
|
|
318
|
+
report_name.column_list.append(ReportColumn(dt, "RecordId", FieldType.LONG))
|
|
319
|
+
results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, report_name, filters,
|
|
320
|
+
page_limit, page_size, page_number)
|
|
321
|
+
else:
|
|
322
|
+
raise SapioException("Unrecognized report object.")
|
|
323
|
+
|
|
324
|
+
# Using the bracket accessor because we want to throw an exception if RecordId doesn't exist in the report.
|
|
325
|
+
# This should only possibly be the case with system reports, as quick reports will include the record ID and
|
|
326
|
+
# we forced any given custom report to have a record ID column.
|
|
181
327
|
ids: list[int] = [row["RecordId"] for row in results]
|
|
182
328
|
return self.query_models_by_id(wrapper_type, ids)
|
|
183
329
|
|
|
@@ -200,7 +346,8 @@ class RecordHandler:
|
|
|
200
346
|
"""
|
|
201
347
|
return self.inst_man.add_new_records_of_type(num, wrapper_type)
|
|
202
348
|
|
|
203
|
-
def add_models_with_data(self, wrapper_type: type[WrappedType], fields: list[
|
|
349
|
+
def add_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldIdentifierMap]) \
|
|
350
|
+
-> list[WrappedType]:
|
|
204
351
|
"""
|
|
205
352
|
Shorthand for using the instance manager to add new models of the given type, and then initializing all those
|
|
206
353
|
models with the given fields.
|
|
@@ -210,11 +357,50 @@ class RecordHandler:
|
|
|
210
357
|
:return: The newly added record models with the provided fields set. The records will be in the same order as
|
|
211
358
|
the fields in the fields list.
|
|
212
359
|
"""
|
|
360
|
+
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
213
361
|
models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
|
|
214
|
-
for model,
|
|
215
|
-
model.set_field_values(
|
|
362
|
+
for model, field_list in zip(models, fields):
|
|
363
|
+
model.set_field_values(field_list)
|
|
216
364
|
return models
|
|
217
365
|
|
|
366
|
+
def find_or_add_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
|
|
367
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType:
|
|
368
|
+
"""
|
|
369
|
+
Find a unique record that matches the given field values. If no such records exist, add a record model to the
|
|
370
|
+
cache with the identifying fields set to the desired values. This record will be created in the system when
|
|
371
|
+
you store and commit changes. If more than one record with the identifying values exists, throws an exception.
|
|
372
|
+
|
|
373
|
+
The record is searched for using the primary identifier field name and value. If multiple records are returned
|
|
374
|
+
by the query on this primary identifier, then the secondary identifiers are used to filter the results.
|
|
375
|
+
|
|
376
|
+
Makes a webservice call to query for the existing record.
|
|
377
|
+
|
|
378
|
+
:param wrapper_type: The record model wrapper to use.
|
|
379
|
+
:param primary_identifier: The data field name of the field to search on.
|
|
380
|
+
:param id_value: The value of the identifying field to search for.
|
|
381
|
+
:param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
|
|
382
|
+
the primary identifier.
|
|
383
|
+
:return: The record model with the identifying field value, either pulled from the system or newly created.
|
|
384
|
+
"""
|
|
385
|
+
# PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
|
|
386
|
+
# If no secondary identifiers were provided, use an empty dictionary.
|
|
387
|
+
if secondary_identifiers is None:
|
|
388
|
+
secondary_identifiers = {}
|
|
389
|
+
|
|
390
|
+
primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
|
|
391
|
+
secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
|
|
392
|
+
unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
|
|
393
|
+
secondary_identifiers)
|
|
394
|
+
# If a unique record matched the identifiers, return it.
|
|
395
|
+
if unique_record is not None:
|
|
396
|
+
return unique_record
|
|
397
|
+
|
|
398
|
+
# If none of the results matched the identifiers, create a new record with all identifiers set.
|
|
399
|
+
# Put the primary identifier and value into the secondary identifiers list and use that as the fields map
|
|
400
|
+
# for this new record.
|
|
401
|
+
secondary_identifiers.update({primary_identifier: id_value})
|
|
402
|
+
return self.add_models_with_data(wrapper_type, [secondary_identifiers])[0]
|
|
403
|
+
|
|
218
404
|
def create_models(self, wrapper_type: type[WrappedType], num: int) -> list[WrappedType]:
|
|
219
405
|
"""
|
|
220
406
|
Shorthand for creating new records via the data record manager and then returning them as wrapped
|
|
@@ -229,7 +415,7 @@ class RecordHandler:
|
|
|
229
415
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
230
416
|
return self.wrap_models(self.dr_man.add_data_records(dt, num), wrapper_type)
|
|
231
417
|
|
|
232
|
-
def create_models_with_data(self, wrapper_type: type[WrappedType], fields: list[
|
|
418
|
+
def create_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldIdentifierMap]) \
|
|
233
419
|
-> list[WrappedType]:
|
|
234
420
|
"""
|
|
235
421
|
Shorthand for creating new records via the data record manager with field data to initialize the records with
|
|
@@ -243,10 +429,12 @@ class RecordHandler:
|
|
|
243
429
|
:return: The newly created record models.
|
|
244
430
|
"""
|
|
245
431
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
432
|
+
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
246
433
|
return self.wrap_models(self.dr_man.add_data_records_with_data(dt, fields), wrapper_type)
|
|
247
434
|
|
|
248
|
-
def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier:
|
|
249
|
-
secondary_identifiers:
|
|
435
|
+
def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
|
|
436
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
|
|
437
|
+
-> WrappedType:
|
|
250
438
|
"""
|
|
251
439
|
Find a unique record that matches the given field values. If no such records exist, create one with the
|
|
252
440
|
identifying fields set to the desired values. If more than one record with the identifying values exists,
|
|
@@ -270,24 +458,10 @@ class RecordHandler:
|
|
|
270
458
|
if secondary_identifiers is None:
|
|
271
459
|
secondary_identifiers = {}
|
|
272
460
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
461
|
+
primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
|
|
462
|
+
secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
|
|
463
|
+
unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
|
|
464
|
+
secondary_identifiers)
|
|
291
465
|
# If a unique record matched the identifiers, return it.
|
|
292
466
|
if unique_record is not None:
|
|
293
467
|
return unique_record
|
|
@@ -329,6 +503,29 @@ class RecordHandler:
|
|
|
329
503
|
return_dict[model] = model.get_parents_of_type(parent_type)
|
|
330
504
|
return return_dict
|
|
331
505
|
|
|
506
|
+
@staticmethod
|
|
507
|
+
def map_by_parent(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
|
|
508
|
+
-> dict[WrappedType, RecordModel]:
|
|
509
|
+
"""
|
|
510
|
+
Take a list of record models and map them by their parent. Essentially an inversion of map_to_parent.
|
|
511
|
+
If two records share the same parent, an exception will be thrown. The parents must already be loaded.
|
|
512
|
+
|
|
513
|
+
:param models: A list of record models.
|
|
514
|
+
:param parent_type: The record model wrapper of the parents.
|
|
515
|
+
:return: A dict[ParentType, ModelType]. If an input model doesn't have a parent of the given parent type,
|
|
516
|
+
then it will not be in the resulting dictionary.
|
|
517
|
+
"""
|
|
518
|
+
to_parent: dict[RecordModel, WrappedType] = RecordHandler.map_to_parent(models, parent_type)
|
|
519
|
+
by_parent: dict[WrappedType, RecordModel] = {}
|
|
520
|
+
for record, parent in to_parent.items():
|
|
521
|
+
if parent is None:
|
|
522
|
+
continue
|
|
523
|
+
if parent in by_parent:
|
|
524
|
+
raise SapioException(f"Parent {parent.data_type_name} {parent.record_id} encountered more than once "
|
|
525
|
+
f"in models list.")
|
|
526
|
+
by_parent[parent] = record
|
|
527
|
+
return by_parent
|
|
528
|
+
|
|
332
529
|
@staticmethod
|
|
333
530
|
def map_by_parents(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
|
|
334
531
|
-> dict[WrappedType, list[RecordModel]]:
|
|
@@ -379,6 +576,29 @@ class RecordHandler:
|
|
|
379
576
|
return_dict[model] = model.get_children_of_type(child_type)
|
|
380
577
|
return return_dict
|
|
381
578
|
|
|
579
|
+
@staticmethod
|
|
580
|
+
def map_by_child(models: Iterable[RecordModel], child_type: type[WrappedType]) \
|
|
581
|
+
-> dict[WrappedType, RecordModel]:
|
|
582
|
+
"""
|
|
583
|
+
Take a list of record models and map them by their children. Essentially an inversion of map_to_child.
|
|
584
|
+
If two records share the same child, an exception will be thrown. The children must already be loaded.
|
|
585
|
+
|
|
586
|
+
:param models: A list of record models.
|
|
587
|
+
:param child_type: The record model wrapper of the children.
|
|
588
|
+
:return: A dict[ChildType, ModelType]. If an input model doesn't have a child of the given child type,
|
|
589
|
+
then it will not be in the resulting dictionary.
|
|
590
|
+
"""
|
|
591
|
+
to_child: dict[RecordModel, WrappedType] = RecordHandler.map_to_child(models, child_type)
|
|
592
|
+
by_child: dict[WrappedType, RecordModel] = {}
|
|
593
|
+
for record, child in to_child.items():
|
|
594
|
+
if child is None:
|
|
595
|
+
continue
|
|
596
|
+
if child in by_child:
|
|
597
|
+
raise SapioException(f"Child {child.data_type_name} {child.record_id} encountered more than once "
|
|
598
|
+
f"in models list.")
|
|
599
|
+
by_child[child] = record
|
|
600
|
+
return by_child
|
|
601
|
+
|
|
382
602
|
@staticmethod
|
|
383
603
|
def map_by_children(models: Iterable[RecordModel], child_type: type[WrappedType]) \
|
|
384
604
|
-> dict[WrappedType, list[RecordModel]]:
|
|
@@ -399,7 +619,7 @@ class RecordHandler:
|
|
|
399
619
|
return by_children
|
|
400
620
|
|
|
401
621
|
@staticmethod
|
|
402
|
-
def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name:
|
|
622
|
+
def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
403
623
|
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
|
|
404
624
|
"""
|
|
405
625
|
Map a list of record models to their forward side link. The forward side link must already be loaded.
|
|
@@ -410,14 +630,15 @@ class RecordHandler:
|
|
|
410
630
|
:return: A dict[ModelType, SlideLink]. If an input model doesn't have a forward side link of the given type,
|
|
411
631
|
then it will map to None.
|
|
412
632
|
"""
|
|
633
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
413
634
|
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
414
635
|
for model in models:
|
|
415
636
|
return_dict[model] = model.get_forward_side_link(field_name, side_link_type)
|
|
416
637
|
return return_dict
|
|
417
638
|
|
|
418
639
|
@staticmethod
|
|
419
|
-
def
|
|
420
|
-
|
|
640
|
+
def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
641
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
421
642
|
"""
|
|
422
643
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
423
644
|
map_to_forward_side_link. Input models that share a forward side link will end up in the same list.
|
|
@@ -429,9 +650,10 @@ class RecordHandler:
|
|
|
429
650
|
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
|
|
430
651
|
pointing to it, then it will not be in the resulting dictionary.
|
|
431
652
|
"""
|
|
432
|
-
|
|
653
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
654
|
+
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
433
655
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
434
|
-
by_side_link: dict[WrappedType, list[
|
|
656
|
+
by_side_link: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
435
657
|
for record, side_link in to_side_link.items():
|
|
436
658
|
if side_link is None:
|
|
437
659
|
continue
|
|
@@ -439,8 +661,35 @@ class RecordHandler:
|
|
|
439
661
|
return by_side_link
|
|
440
662
|
|
|
441
663
|
@staticmethod
|
|
442
|
-
def
|
|
443
|
-
|
|
664
|
+
def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
665
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
|
|
666
|
+
"""
|
|
667
|
+
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
668
|
+
map_to_forward_side_link, but if two records share the same forward link, an exception is thrown.
|
|
669
|
+
The forward side link must already be loaded.
|
|
670
|
+
|
|
671
|
+
:param models: A list of record models.
|
|
672
|
+
:param field_name: The field name on the record models where the side link is located.
|
|
673
|
+
:param side_link_type: The record model wrapper of the forward side links.
|
|
674
|
+
:return: A dict[SideLink, ModelType]. If an input model doesn't have a forward side link of the given type
|
|
675
|
+
pointing to it, then it will not be in the resulting dictionary.
|
|
676
|
+
"""
|
|
677
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
678
|
+
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
679
|
+
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
680
|
+
by_side_link: dict[WrappedType, WrappedRecordModel] = {}
|
|
681
|
+
for record, side_link in to_side_link.items():
|
|
682
|
+
if side_link is None:
|
|
683
|
+
continue
|
|
684
|
+
if side_link in by_side_link:
|
|
685
|
+
raise SapioException(f"Side link {side_link.data_type_name} {side_link.record_id} encountered more "
|
|
686
|
+
f"than once in models list.")
|
|
687
|
+
by_side_link[side_link] = record
|
|
688
|
+
return by_side_link
|
|
689
|
+
|
|
690
|
+
@staticmethod
|
|
691
|
+
def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
692
|
+
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, list[WrappedType]]:
|
|
444
693
|
"""
|
|
445
694
|
Map a list of record models to a list reverse side links of a given type. The reverse side links must already
|
|
446
695
|
be loaded.
|
|
@@ -452,13 +701,38 @@ class RecordHandler:
|
|
|
452
701
|
:return: A dict[ModelType, list[SideLink]]. If an input model doesn't have reverse side links of the given type,
|
|
453
702
|
then it will map to an empty list.
|
|
454
703
|
"""
|
|
704
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
455
705
|
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
456
706
|
for model in models:
|
|
457
707
|
return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
|
|
458
708
|
return return_dict
|
|
459
709
|
|
|
460
710
|
@staticmethod
|
|
461
|
-
def
|
|
711
|
+
def map_to_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
712
|
+
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
|
|
713
|
+
"""
|
|
714
|
+
Map a list of record models to the reverse side link of a given type. If a given record has more than one
|
|
715
|
+
reverse side link of this type, an exception is thrown. The reverse side links must already be loaded.
|
|
716
|
+
|
|
717
|
+
:param models: A list of record models.
|
|
718
|
+
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
719
|
+
located.
|
|
720
|
+
:param side_link_type: The record model wrapper of the reverse side links.
|
|
721
|
+
:return: A dict[ModelType, SideLink]. If an input model doesn't have reverse side links of the given type,
|
|
722
|
+
then it will map to None.
|
|
723
|
+
"""
|
|
724
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
725
|
+
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
726
|
+
for model in models:
|
|
727
|
+
links: list[WrappedType] = model.get_reverse_side_link(field_name, side_link_type)
|
|
728
|
+
if len(links) > 1:
|
|
729
|
+
raise SapioException(f"Model {model.data_type_name} {model.record_id} has more than one reverse link "
|
|
730
|
+
f"of type {side_link_type.get_wrapper_data_type_name()}.")
|
|
731
|
+
return_dict[model] = links[0] if links else None
|
|
732
|
+
return return_dict
|
|
733
|
+
|
|
734
|
+
@staticmethod
|
|
735
|
+
def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
462
736
|
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
463
737
|
"""
|
|
464
738
|
Take a list of record models and map them by their reverse side links. Essentially an inversion of
|
|
@@ -472,14 +746,43 @@ class RecordHandler:
|
|
|
472
746
|
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
|
|
473
747
|
pointing to it, then it will not be in the resulting dictionary.
|
|
474
748
|
"""
|
|
475
|
-
|
|
749
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
750
|
+
to_side_links: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler\
|
|
476
751
|
.map_to_reverse_side_links(models, field_name, side_link_type)
|
|
477
|
-
by_side_links: dict[WrappedType, list[
|
|
752
|
+
by_side_links: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
478
753
|
for record, side_links in to_side_links.items():
|
|
479
754
|
for side_link in side_links:
|
|
480
755
|
by_side_links.setdefault(side_link, []).append(record)
|
|
481
756
|
return by_side_links
|
|
482
757
|
|
|
758
|
+
@staticmethod
|
|
759
|
+
def map_by_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
760
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
|
|
761
|
+
"""
|
|
762
|
+
Take a list of record models and map them by their reverse side link. Essentially an inversion of
|
|
763
|
+
map_to_reverse_side_link. If two records share the same reverse side link, an exception is thrown.
|
|
764
|
+
The reverse side links must already be loaded.
|
|
765
|
+
|
|
766
|
+
:param models: A list of record models.
|
|
767
|
+
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
768
|
+
located.
|
|
769
|
+
:param side_link_type: The record model wrapper of the reverse side links.
|
|
770
|
+
:return: A dict[SideLink, ModelType]. If an input model doesn't have a reverse side link of the given type
|
|
771
|
+
pointing to it, then it will not be in the resulting dictionary.
|
|
772
|
+
"""
|
|
773
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
774
|
+
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
775
|
+
.map_to_reverse_side_link(models, field_name, side_link_type)
|
|
776
|
+
by_side_link: dict[WrappedType, WrappedRecordModel] = {}
|
|
777
|
+
for record, side_link in to_side_link.items():
|
|
778
|
+
if side_link is None:
|
|
779
|
+
continue
|
|
780
|
+
if side_link in by_side_link:
|
|
781
|
+
raise SapioException(f"Side link {side_link.data_type_name} {side_link.record_id} encountered more "
|
|
782
|
+
f"than once in models list.")
|
|
783
|
+
by_side_link[side_link] = record
|
|
784
|
+
return by_side_link
|
|
785
|
+
|
|
483
786
|
@staticmethod
|
|
484
787
|
def map_by_id(models: Iterable[SapioRecord]) -> dict[int, SapioRecord]:
|
|
485
788
|
"""
|
|
@@ -494,7 +797,8 @@ class RecordHandler:
|
|
|
494
797
|
return ret_dict
|
|
495
798
|
|
|
496
799
|
@staticmethod
|
|
497
|
-
def map_by_field(models: Iterable[SapioRecord], field_name:
|
|
800
|
+
def map_by_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
|
|
801
|
+
-> dict[FieldValue, list[SapioRecord]]:
|
|
498
802
|
"""
|
|
499
803
|
Map the given records by one of their fields. If any two records share the same field value, they'll appear in
|
|
500
804
|
the same value list.
|
|
@@ -503,14 +807,16 @@ class RecordHandler:
|
|
|
503
807
|
:param field_name: The field name to map against.
|
|
504
808
|
:return: A dict mapping field values to the records with that value.
|
|
505
809
|
"""
|
|
506
|
-
|
|
810
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
811
|
+
ret_dict: dict[FieldValue, list[SapioRecord]] = {}
|
|
507
812
|
for model in models:
|
|
508
|
-
val:
|
|
813
|
+
val: FieldValue = model.get_field_value(field_name)
|
|
509
814
|
ret_dict.setdefault(val, []).append(model)
|
|
510
815
|
return ret_dict
|
|
511
816
|
|
|
512
817
|
@staticmethod
|
|
513
|
-
def map_by_unique_field(models: Iterable[SapioRecord], field_name:
|
|
818
|
+
def map_by_unique_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
|
|
819
|
+
-> dict[FieldValue, SapioRecord]:
|
|
514
820
|
"""
|
|
515
821
|
Uniquely map the given records by one of their fields. If any two records share the same field value, throws
|
|
516
822
|
an exception.
|
|
@@ -519,16 +825,17 @@ class RecordHandler:
|
|
|
519
825
|
:param field_name: The field name to map against.
|
|
520
826
|
:return: A dict mapping field values to the record with that value.
|
|
521
827
|
"""
|
|
522
|
-
|
|
828
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
829
|
+
ret_dict: dict[FieldValue, SapioRecord] = {}
|
|
523
830
|
for model in models:
|
|
524
|
-
val:
|
|
831
|
+
val: FieldValue = model.get_field_value(field_name)
|
|
525
832
|
if val in ret_dict:
|
|
526
833
|
raise SapioException(f"Value {val} encountered more than once in models list.")
|
|
527
834
|
ret_dict.update({val: model})
|
|
528
835
|
return ret_dict
|
|
529
836
|
|
|
530
837
|
@staticmethod
|
|
531
|
-
def sum_of_field(models: Iterable[SapioRecord], field_name:
|
|
838
|
+
def sum_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
532
839
|
"""
|
|
533
840
|
Sum up the numeric value of a given field across all input models. Excepts that all given models have a value.
|
|
534
841
|
If the field is an integer field, the value will be converted to a float.
|
|
@@ -537,13 +844,14 @@ class RecordHandler:
|
|
|
537
844
|
:param field_name: The name of the numeric field to sum.
|
|
538
845
|
:return: The sum of the field values for the collection of models.
|
|
539
846
|
"""
|
|
847
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
540
848
|
field_sum: float = 0
|
|
541
849
|
for model in models:
|
|
542
850
|
field_sum += float(model.get_field_value(field_name))
|
|
543
851
|
return field_sum
|
|
544
852
|
|
|
545
853
|
@staticmethod
|
|
546
|
-
def mean_of_field(models: Iterable[SapioRecord], field_name:
|
|
854
|
+
def mean_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
547
855
|
"""
|
|
548
856
|
Calculate the mean of the numeric value of a given field across all input models. Excepts that all given models
|
|
549
857
|
have a value. If the field is an integer field, the value will be converted to a float.
|
|
@@ -584,8 +892,8 @@ class RecordHandler:
|
|
|
584
892
|
return oldest
|
|
585
893
|
|
|
586
894
|
@staticmethod
|
|
587
|
-
def values_to_field_maps(field_name:
|
|
588
|
-
|
|
895
|
+
def values_to_field_maps(field_name: FieldIdentifier, values: Iterable[FieldValue],
|
|
896
|
+
existing_fields: list[FieldIdentifier] | None = None) -> list[FieldMap]:
|
|
589
897
|
"""
|
|
590
898
|
Add a list of values for a specific field to a list of dictionaries pairing each value to that field name.
|
|
591
899
|
|
|
@@ -596,6 +904,8 @@ class RecordHandler:
|
|
|
596
904
|
:return: A fields map list that contains the given values mapped by the given field name.
|
|
597
905
|
"""
|
|
598
906
|
# Update the existing fields map list if one is given.
|
|
907
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
908
|
+
existing_fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(existing_fields)
|
|
599
909
|
if existing_fields:
|
|
600
910
|
values = list(values)
|
|
601
911
|
# The number of new values must match the length of the existing fields list.
|
|
@@ -616,8 +926,6 @@ class RecordHandler:
|
|
|
616
926
|
path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
|
|
617
927
|
relationship path must already be loaded.
|
|
618
928
|
|
|
619
|
-
Currently, the relationship path may only contain parent/child nodes.
|
|
620
|
-
|
|
621
929
|
:param models: A list of record models.
|
|
622
930
|
:param path: The relationship path to follow.
|
|
623
931
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -628,15 +936,44 @@ class RecordHandler:
|
|
|
628
936
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
629
937
|
path: list[RelationshipNode] = path.path
|
|
630
938
|
for model in models:
|
|
631
|
-
current: PyRecordModel = model if isinstance(model, PyRecordModel) else model.backing_model
|
|
939
|
+
current: PyRecordModel | None = model if isinstance(model, PyRecordModel) else model.backing_model
|
|
632
940
|
for node in path:
|
|
633
|
-
|
|
941
|
+
data_type: str = node.data_type_name
|
|
942
|
+
direction: RelationshipNodeType = node.direction
|
|
634
943
|
if current is None:
|
|
635
944
|
break
|
|
636
945
|
if direction == RelationshipNodeType.CHILD:
|
|
637
|
-
current = current.get_child_of_type(
|
|
946
|
+
current = current.get_child_of_type(data_type)
|
|
638
947
|
elif direction == RelationshipNodeType.PARENT:
|
|
639
|
-
current = current.get_parent_of_type(
|
|
948
|
+
current = current.get_parent_of_type(data_type)
|
|
949
|
+
elif direction == RelationshipNodeType.ANCESTOR:
|
|
950
|
+
ancestors: list[PyRecordModel] = list(self.an_man.get_ancestors_of_type(current, data_type))
|
|
951
|
+
if not ancestors:
|
|
952
|
+
current = None
|
|
953
|
+
elif len(ancestors) > 1:
|
|
954
|
+
raise SapioException(f"Hierarchy contains multiple ancestors of type {data_type}.")
|
|
955
|
+
else:
|
|
956
|
+
current = ancestors[0]
|
|
957
|
+
elif direction == RelationshipNodeType.DESCENDANT:
|
|
958
|
+
descendants: list[PyRecordModel] = list(self.an_man.get_descendant_of_type(current, data_type))
|
|
959
|
+
if not descendants:
|
|
960
|
+
current = None
|
|
961
|
+
elif len(descendants) > 1:
|
|
962
|
+
raise SapioException(f"Hierarchy contains multiple descendants of type {data_type}.")
|
|
963
|
+
else:
|
|
964
|
+
current = descendants[0]
|
|
965
|
+
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
966
|
+
current = current.get_forward_side_link(node.data_field_name)
|
|
967
|
+
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
968
|
+
field_name: str = node.data_field_name
|
|
969
|
+
reverse_links: list[PyRecordModel] = current.get_reverse_side_link(field_name, data_type)
|
|
970
|
+
if not reverse_links:
|
|
971
|
+
current = None
|
|
972
|
+
elif len(reverse_links) > 1:
|
|
973
|
+
raise SapioException(f"Hierarchy contains multiple reverse links of type {data_type} on field "
|
|
974
|
+
f"{field_name}.")
|
|
975
|
+
else:
|
|
976
|
+
current = reverse_links[0]
|
|
640
977
|
else:
|
|
641
978
|
raise SapioException("Unsupported path direction.")
|
|
642
979
|
ret_dict.update({model: self.inst_man.wrap(current, wrapper_type) if current else None})
|
|
@@ -649,8 +986,6 @@ class RecordHandler:
|
|
|
649
986
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
650
987
|
relationship path must already be loaded.
|
|
651
988
|
|
|
652
|
-
Currently, the relationship path may only contain parent/child nodes.
|
|
653
|
-
|
|
654
989
|
:param models: A list of record models.
|
|
655
990
|
:param path: The relationship path to follow.
|
|
656
991
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -665,14 +1000,23 @@ class RecordHandler:
|
|
|
665
1000
|
next_search: set[PyRecordModel] = set()
|
|
666
1001
|
# Exhaust the records at each step in the path, then use those records for the next step.
|
|
667
1002
|
for node in path:
|
|
668
|
-
|
|
1003
|
+
data_type: str = node.data_type_name
|
|
1004
|
+
direction: RelationshipNodeType = node.direction
|
|
669
1005
|
if len(current_search) == 0:
|
|
670
1006
|
break
|
|
671
1007
|
for search in current_search:
|
|
672
1008
|
if direction == RelationshipNodeType.CHILD:
|
|
673
|
-
next_search.update(search.get_children_of_type(
|
|
1009
|
+
next_search.update(search.get_children_of_type(data_type))
|
|
674
1010
|
elif direction == RelationshipNodeType.PARENT:
|
|
675
|
-
next_search.update(search.get_parents_of_type(
|
|
1011
|
+
next_search.update(search.get_parents_of_type(data_type))
|
|
1012
|
+
elif direction == RelationshipNodeType.ANCESTOR:
|
|
1013
|
+
next_search.update(self.an_man.get_ancestors_of_type(search, data_type))
|
|
1014
|
+
elif direction == RelationshipNodeType.DESCENDANT:
|
|
1015
|
+
next_search.update(self.an_man.get_descendant_of_type(search, data_type))
|
|
1016
|
+
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
1017
|
+
next_search.add(search.get_forward_side_link(node.data_field_name))
|
|
1018
|
+
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
1019
|
+
next_search.update(search.get_reverse_side_link(node.data_field_name, data_type))
|
|
676
1020
|
else:
|
|
677
1021
|
raise SapioException("Unsupported path direction.")
|
|
678
1022
|
current_search = next_search
|
|
@@ -690,11 +1034,9 @@ class RecordHandler:
|
|
|
690
1034
|
relationship path must already be loaded.
|
|
691
1035
|
|
|
692
1036
|
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
|
|
1037
|
+
relationships (e.g. a sample which is aliquoted to a number of samples, then those aliquots are pooled back
|
|
694
1038
|
together into a single sample).
|
|
695
1039
|
|
|
696
|
-
Currently, the relationship path may only contain parent/child nodes.
|
|
697
|
-
|
|
698
1040
|
:param models: A list of record models.
|
|
699
1041
|
:param path: The relationship path to follow.
|
|
700
1042
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -707,22 +1049,64 @@ class RecordHandler:
|
|
|
707
1049
|
for model in models:
|
|
708
1050
|
current: list[PyRecordModel] = [model if isinstance(model, PyRecordModel) else model.backing_model]
|
|
709
1051
|
for node in path:
|
|
710
|
-
|
|
1052
|
+
data_type: str = node.data_type_name
|
|
1053
|
+
direction: RelationshipNodeType = node.direction
|
|
711
1054
|
if len(current) == 0:
|
|
712
1055
|
break
|
|
713
1056
|
if direction == RelationshipNodeType.CHILD:
|
|
714
|
-
current = current[0].get_children_of_type(
|
|
1057
|
+
current = current[0].get_children_of_type(data_type)
|
|
715
1058
|
elif direction == RelationshipNodeType.PARENT:
|
|
716
|
-
current = current[0].get_parents_of_type(
|
|
1059
|
+
current = current[0].get_parents_of_type(data_type)
|
|
1060
|
+
elif direction == RelationshipNodeType.ANCESTOR:
|
|
1061
|
+
current = list(self.an_man.get_ancestors_of_type(current[0], data_type))
|
|
1062
|
+
elif direction == RelationshipNodeType.DESCENDANT:
|
|
1063
|
+
current = list(self.an_man.get_descendant_of_type(current[0], data_type))
|
|
1064
|
+
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
1065
|
+
current = [current[0].get_forward_side_link(node.data_field_name)]
|
|
1066
|
+
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
1067
|
+
current = current[0].get_reverse_side_link(node.data_field_name, data_type)
|
|
717
1068
|
else:
|
|
718
1069
|
raise SapioException("Unsupported path direction.")
|
|
719
1070
|
ret_dict.update({model: self.inst_man.wrap(current[0], wrapper_type) if current else None})
|
|
720
1071
|
return ret_dict
|
|
721
1072
|
|
|
722
|
-
def
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1073
|
+
def __find_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value: FieldValue,
|
|
1074
|
+
secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType | None:
|
|
1075
|
+
"""
|
|
1076
|
+
Find a record from the system that matches the given field values. The primary identifier and value is used
|
|
1077
|
+
to query for the record, then the secondary identifiers may be optionally provided to further filter the
|
|
1078
|
+
returned results. If no record is found with these filters, returns None.
|
|
1079
|
+
"""
|
|
1080
|
+
# Query for all records that match the primary identifier.
|
|
1081
|
+
results: list[WrappedType] = self.query_models(wrapper_type, primary_identifier, [id_value])
|
|
1082
|
+
|
|
1083
|
+
# Find the one record, if any, that matches the secondary identifiers.
|
|
1084
|
+
unique_record: WrappedType | None = None
|
|
1085
|
+
for result in results:
|
|
1086
|
+
matches_all: bool = True
|
|
1087
|
+
for field, value in secondary_identifiers.items():
|
|
1088
|
+
if result.get_field_value(field) != value:
|
|
1089
|
+
matches_all = False
|
|
1090
|
+
break
|
|
1091
|
+
if matches_all:
|
|
1092
|
+
# If a previous record in the results already matched all identifiers, then throw an exception.
|
|
1093
|
+
if unique_record is not None:
|
|
1094
|
+
raise SapioException(f"More than one record of type {wrapper_type.get_wrapper_data_type_name()} "
|
|
1095
|
+
f"encountered in system that matches all provided identifiers.")
|
|
1096
|
+
unique_record = result
|
|
1097
|
+
return unique_record
|
|
1098
|
+
|
|
1099
|
+
@staticmethod
|
|
1100
|
+
def __verify_data_type(records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> None:
|
|
1101
|
+
"""
|
|
1102
|
+
Throw an exception if the data type of the given records and wrapper don't match.
|
|
1103
|
+
"""
|
|
1104
|
+
model_type: str = wrapper_type.get_wrapper_data_type_name()
|
|
1105
|
+
for record in records:
|
|
1106
|
+
record_type: str = record.data_type_name
|
|
1107
|
+
# Account for ELN data type records.
|
|
1108
|
+
if ElnBaseDataType.is_eln_type(record_type):
|
|
1109
|
+
record_type = ElnBaseDataType.get_base_type(record_type).data_type_name
|
|
1110
|
+
if record_type != model_type:
|
|
1111
|
+
raise SapioException(f"Data record of type {record_type} cannot be wrapped by the record model wrapper "
|
|
1112
|
+
f"of type {model_type}")
|