sapiopycommons 2024.11.8a355__py3-none-any.whl → 2024.11.8a359__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 +83 -532
- sapiopycommons/chem/IndigoMolecules.py +0 -2
- sapiopycommons/chem/Molecules.py +18 -77
- sapiopycommons/datatype/attachment_util.py +10 -11
- sapiopycommons/eln/experiment_handler.py +70 -272
- sapiopycommons/files/complex_data_loader.py +4 -5
- sapiopycommons/files/file_bridge.py +24 -31
- sapiopycommons/files/file_data_handler.py +5 -2
- sapiopycommons/files/file_util.py +9 -59
- sapiopycommons/files/file_validator.py +6 -92
- sapiopycommons/files/file_writer.py +15 -44
- sapiopycommons/general/aliases.py +6 -207
- sapiopycommons/general/custom_report_util.py +37 -212
- sapiopycommons/general/exceptions.py +8 -21
- sapiopycommons/general/popup_util.py +0 -21
- sapiopycommons/general/time_util.py +2 -8
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +97 -481
- sapiopycommons/rules/eln_rule_handler.py +25 -34
- sapiopycommons/rules/on_save_rule_handler.py +31 -34
- sapiopycommons/webhook/webhook_handlers.py +42 -201
- {sapiopycommons-2024.11.8a355.dist-info → sapiopycommons-2024.11.8a359.dist-info}/METADATA +2 -4
- sapiopycommons-2024.11.8a359.dist-info/RECORD +38 -0
- sapiopycommons/callbacks/field_builder.py +0 -537
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +0 -60
- sapiopycommons/customreport/custom_report_builder.py +0 -130
- sapiopycommons/customreport/term_builder.py +0 -299
- sapiopycommons/datatype/data_fields.py +0 -61
- sapiopycommons/datatype/pseudo_data_types.py +0 -440
- sapiopycommons/eln/experiment_report_util.py +0 -653
- sapiopycommons/files/file_bridge_handler.py +0 -340
- sapiopycommons/flowcyto/flow_cyto.py +0 -77
- sapiopycommons/flowcyto/flowcyto_data.py +0 -75
- sapiopycommons/general/accession_service.py +0 -375
- sapiopycommons/general/audit_log.py +0 -189
- sapiopycommons/general/sapio_links.py +0 -50
- sapiopycommons/multimodal/multimodal.py +0 -146
- sapiopycommons/multimodal/multimodal_data.py +0 -489
- sapiopycommons/processtracking/custom_workflow_handler.py +0 -406
- sapiopycommons/sftpconnect/__init__.py +0 -0
- sapiopycommons/sftpconnect/sftp_builder.py +0 -69
- sapiopycommons/webhook/webhook_context.py +0 -39
- sapiopycommons/webhook/webservice_handlers.py +0 -67
- sapiopycommons-2024.11.8a355.dist-info/RECORD +0 -59
- {sapiopycommons-2024.11.8a355.dist-info → sapiopycommons-2024.11.8a359.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.11.8a355.dist-info → sapiopycommons-2024.11.8a359.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
from collections.abc import Iterable
|
|
4
|
-
from
|
|
2
|
+
from typing import Any
|
|
5
3
|
|
|
6
4
|
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
7
5
|
from sapiopylib.rest.User import SapioUser
|
|
8
|
-
from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, RawReportTerm, ReportColumn
|
|
9
6
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
10
7
|
from sapiopylib.rest.pojo.DataRecordPaging import DataRecordPojoPageCriteria
|
|
11
|
-
from sapiopylib.rest.pojo.
|
|
12
|
-
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
8
|
+
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
13
9
|
from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
|
|
14
10
|
QueryAllRecordsOfTypeAutoPager
|
|
15
11
|
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
@@ -18,10 +14,8 @@ from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelMana
|
|
|
18
14
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, WrappedRecordModel
|
|
19
15
|
from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
|
|
20
16
|
RelationshipNodeType
|
|
21
|
-
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
22
17
|
|
|
23
|
-
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap
|
|
24
|
-
FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey
|
|
18
|
+
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap
|
|
25
19
|
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
26
20
|
from sapiopycommons.general.exceptions import SapioException
|
|
27
21
|
|
|
@@ -36,38 +30,16 @@ class RecordHandler:
|
|
|
36
30
|
rec_man: RecordModelManager
|
|
37
31
|
inst_man: RecordModelInstanceManager
|
|
38
32
|
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
|
|
55
33
|
|
|
56
|
-
def __init__(self, context:
|
|
34
|
+
def __init__(self, context: SapioWebhookContext | SapioUser):
|
|
57
35
|
"""
|
|
58
36
|
:param context: The current webhook context or a user object to send requests from.
|
|
59
37
|
"""
|
|
60
|
-
self.user = AliasUtil.to_sapio_user(context)
|
|
61
|
-
if self.__initialized:
|
|
62
|
-
return
|
|
63
|
-
self.__initialized = True
|
|
64
|
-
|
|
65
38
|
self.user = context if isinstance(context, SapioUser) else context.user
|
|
66
39
|
self.dr_man = DataRecordManager(self.user)
|
|
67
40
|
self.rec_man = RecordModelManager(self.user)
|
|
68
41
|
self.inst_man = self.rec_man.instance_manager
|
|
69
42
|
self.rel_man = self.rec_man.relationship_manager
|
|
70
|
-
self.an_man = RecordModelAncestorManager(self.rec_man)
|
|
71
43
|
|
|
72
44
|
def wrap_model(self, record: DataRecord, wrapper_type: type[WrappedType]) -> WrappedType:
|
|
73
45
|
"""
|
|
@@ -77,7 +49,6 @@ class RecordHandler:
|
|
|
77
49
|
:param wrapper_type: The record model wrapper to use.
|
|
78
50
|
:return: The record model for the input.
|
|
79
51
|
"""
|
|
80
|
-
self.__verify_data_type([record], wrapper_type)
|
|
81
52
|
return self.inst_man.add_existing_record_of_type(record, wrapper_type)
|
|
82
53
|
|
|
83
54
|
def wrap_models(self, records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> list[WrappedType]:
|
|
@@ -88,11 +59,10 @@ class RecordHandler:
|
|
|
88
59
|
:param wrapper_type: The record model wrapper to use.
|
|
89
60
|
:return: The record models for the input.
|
|
90
61
|
"""
|
|
91
|
-
self.__verify_data_type(records, wrapper_type)
|
|
92
62
|
return self.inst_man.add_existing_records_of_type(list(records), wrapper_type)
|
|
93
63
|
|
|
94
|
-
def query_models(self, wrapper_type: type[WrappedType], field:
|
|
95
|
-
page_limit: int | None = None
|
|
64
|
+
def query_models(self, wrapper_type: type[WrappedType], field: str, value_list: Iterable[Any],
|
|
65
|
+
page_limit: int | None = None) -> list[WrappedType]:
|
|
96
66
|
"""
|
|
97
67
|
Shorthand for using the data record manager to query for a list of data records by field value
|
|
98
68
|
and then converting the results into a list of record models.
|
|
@@ -100,63 +70,12 @@ class RecordHandler:
|
|
|
100
70
|
:param wrapper_type: The record model wrapper to use.
|
|
101
71
|
:param field: The field to query on.
|
|
102
72
|
:param value_list: The values of the field to query on.
|
|
103
|
-
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
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.
|
|
73
|
+
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
106
74
|
:return: The record models for the queried records.
|
|
107
75
|
"""
|
|
108
|
-
|
|
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]
|
|
76
|
+
return self.query_models_with_criteria(wrapper_type, field, value_list, None, page_limit)[0]
|
|
112
77
|
|
|
113
|
-
def
|
|
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],
|
|
78
|
+
def query_models_with_criteria(self, wrapper_type: type[WrappedType], field: str, value_list: Iterable[Any],
|
|
160
79
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
161
80
|
page_limit: int | None = None) \
|
|
162
81
|
-> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
|
|
@@ -169,33 +88,26 @@ class RecordHandler:
|
|
|
169
88
|
:param value_list: The values of the field to query on.
|
|
170
89
|
:param paging_criteria: The paging criteria to start the query with.
|
|
171
90
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
172
|
-
possible pages.
|
|
173
|
-
enforces a page size.
|
|
91
|
+
possible pages.
|
|
174
92
|
:return: The record models for the queried records and the final paging criteria.
|
|
175
93
|
"""
|
|
176
94
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
177
|
-
field: str = AliasUtil.to_data_field_name(field)
|
|
178
95
|
pager = QueryDataRecordsAutoPager(dt, field, list(value_list), self.user, paging_criteria)
|
|
179
96
|
pager.max_page = page_limit
|
|
180
97
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
181
98
|
|
|
182
99
|
def query_models_by_id(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
183
|
-
page_limit: int | None = None
|
|
100
|
+
page_limit: int | None = None) -> list[WrappedType]:
|
|
184
101
|
"""
|
|
185
102
|
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
186
103
|
and then converting the results into a list of record models.
|
|
187
104
|
|
|
188
105
|
:param wrapper_type: The record model wrapper to use.
|
|
189
106
|
:param ids: The list of record IDs to query.
|
|
190
|
-
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
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.
|
|
107
|
+
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
193
108
|
:return: The record models for the queried records.
|
|
194
109
|
"""
|
|
195
|
-
|
|
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]
|
|
110
|
+
return self.query_models_by_id_with_criteria(wrapper_type, ids, None, page_limit)[0]
|
|
199
111
|
|
|
200
112
|
def query_models_by_id_with_criteria(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
201
113
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
@@ -209,8 +121,7 @@ class RecordHandler:
|
|
|
209
121
|
:param ids: The list of record IDs to query.
|
|
210
122
|
:param paging_criteria: The paging criteria to start the query with.
|
|
211
123
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
212
|
-
possible pages.
|
|
213
|
-
enforces a page size.
|
|
124
|
+
possible pages.
|
|
214
125
|
:return: The record models for the queried records and the final paging criteria.
|
|
215
126
|
"""
|
|
216
127
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
@@ -218,38 +129,16 @@ class RecordHandler:
|
|
|
218
129
|
pager.max_page = page_limit
|
|
219
130
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
220
131
|
|
|
221
|
-
def
|
|
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]:
|
|
132
|
+
def query_all_models(self, wrapper_type: type[WrappedType], page_limit: int | None = None) -> list[WrappedType]:
|
|
239
133
|
"""
|
|
240
134
|
Shorthand for using the data record manager to query for all data records of a given type
|
|
241
135
|
and then converting the results into a list of record models.
|
|
242
136
|
|
|
243
137
|
:param wrapper_type: The record model wrapper to use.
|
|
244
|
-
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
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.
|
|
138
|
+
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
247
139
|
:return: The record models for the queried records.
|
|
248
140
|
"""
|
|
249
|
-
|
|
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]
|
|
141
|
+
return self.query_all_models_with_criteria(wrapper_type, None, page_limit)[0]
|
|
253
142
|
|
|
254
143
|
def query_all_models_with_criteria(self, wrapper_type: type[WrappedType],
|
|
255
144
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
@@ -262,8 +151,7 @@ class RecordHandler:
|
|
|
262
151
|
:param wrapper_type: The record model wrapper to use.
|
|
263
152
|
:param paging_criteria: The paging criteria to start the query with.
|
|
264
153
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
265
|
-
possible pages.
|
|
266
|
-
enforces a page size.
|
|
154
|
+
possible pages.
|
|
267
155
|
:return: The record models for the queried records and the final paging criteria.
|
|
268
156
|
"""
|
|
269
157
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
@@ -272,58 +160,24 @@ class RecordHandler:
|
|
|
272
160
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
273
161
|
|
|
274
162
|
def query_models_by_report(self, wrapper_type: type[WrappedType],
|
|
275
|
-
report_name: str
|
|
276
|
-
filters: dict[
|
|
277
|
-
page_limit: int | None = None
|
|
278
|
-
page_size: int | None = None,
|
|
279
|
-
page_number: int | None = None) -> list[WrappedType]:
|
|
163
|
+
report_name: str,
|
|
164
|
+
filters: dict[str, Iterable[Any]] | None = None,
|
|
165
|
+
page_limit: int | None = None) -> list[WrappedType]:
|
|
280
166
|
"""
|
|
281
|
-
Run a report
|
|
282
|
-
First runs the report, then runs a data record manager query on the results of the custom report.
|
|
167
|
+
Run a system report that contains a RecordId column and query for the records with those IDs.
|
|
168
|
+
First runs the custom report, then runs a data record manager query on the results of the custom report.
|
|
283
169
|
|
|
284
|
-
Will throw an exception if
|
|
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.
|
|
170
|
+
Will throw an exception if the given system report does not have a RecordId column.
|
|
288
171
|
|
|
289
172
|
:param wrapper_type: The record model wrapper to use.
|
|
290
|
-
:param report_name: The name of
|
|
291
|
-
criteria for a custom report.
|
|
173
|
+
:param report_name: The name of the system report to run.
|
|
292
174
|
:param filters: If provided, filter the results of the report using the given mapping of headers to values to
|
|
293
175
|
filter on. This filtering is done before the records are queried.
|
|
294
176
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
295
|
-
:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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.
|
|
177
|
+
:return: The record models for the queried records.
|
|
178
|
+
"""
|
|
179
|
+
results: list[dict[str, Any]] = CustomReportUtil.run_system_report(self.user, report_name, filters, page_limit)
|
|
180
|
+
# Using the bracket operators because we want to throw an exception if RecordId doesn't exist in the report.
|
|
327
181
|
ids: list[int] = [row["RecordId"] for row in results]
|
|
328
182
|
return self.query_models_by_id(wrapper_type, ids)
|
|
329
183
|
|
|
@@ -346,8 +200,7 @@ class RecordHandler:
|
|
|
346
200
|
"""
|
|
347
201
|
return self.inst_man.add_new_records_of_type(num, wrapper_type)
|
|
348
202
|
|
|
349
|
-
def add_models_with_data(self, wrapper_type: type[WrappedType], fields: list[
|
|
350
|
-
-> list[WrappedType]:
|
|
203
|
+
def add_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldMap]) -> list[WrappedType]:
|
|
351
204
|
"""
|
|
352
205
|
Shorthand for using the instance manager to add new models of the given type, and then initializing all those
|
|
353
206
|
models with the given fields.
|
|
@@ -357,50 +210,11 @@ class RecordHandler:
|
|
|
357
210
|
:return: The newly added record models with the provided fields set. The records will be in the same order as
|
|
358
211
|
the fields in the fields list.
|
|
359
212
|
"""
|
|
360
|
-
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
361
213
|
models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
|
|
362
|
-
for model,
|
|
363
|
-
model.set_field_values(
|
|
214
|
+
for model, field in zip(models, fields):
|
|
215
|
+
model.set_field_values(field)
|
|
364
216
|
return models
|
|
365
217
|
|
|
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
|
-
|
|
404
218
|
def create_models(self, wrapper_type: type[WrappedType], num: int) -> list[WrappedType]:
|
|
405
219
|
"""
|
|
406
220
|
Shorthand for creating new records via the data record manager and then returning them as wrapped
|
|
@@ -415,7 +229,7 @@ class RecordHandler:
|
|
|
415
229
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
416
230
|
return self.wrap_models(self.dr_man.add_data_records(dt, num), wrapper_type)
|
|
417
231
|
|
|
418
|
-
def create_models_with_data(self, wrapper_type: type[WrappedType], fields: list[
|
|
232
|
+
def create_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldMap]) \
|
|
419
233
|
-> list[WrappedType]:
|
|
420
234
|
"""
|
|
421
235
|
Shorthand for creating new records via the data record manager with field data to initialize the records with
|
|
@@ -429,12 +243,10 @@ class RecordHandler:
|
|
|
429
243
|
:return: The newly created record models.
|
|
430
244
|
"""
|
|
431
245
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
432
|
-
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
433
246
|
return self.wrap_models(self.dr_man.add_data_records_with_data(dt, fields), wrapper_type)
|
|
434
247
|
|
|
435
|
-
def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier:
|
|
436
|
-
|
|
437
|
-
-> WrappedType:
|
|
248
|
+
def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value: Any,
|
|
249
|
+
secondary_identifiers: FieldMap | None = None) -> WrappedType:
|
|
438
250
|
"""
|
|
439
251
|
Find a unique record that matches the given field values. If no such records exist, create one with the
|
|
440
252
|
identifying fields set to the desired values. If more than one record with the identifying values exists,
|
|
@@ -458,10 +270,24 @@ class RecordHandler:
|
|
|
458
270
|
if secondary_identifiers is None:
|
|
459
271
|
secondary_identifiers = {}
|
|
460
272
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
273
|
+
# Query for all records that match the primary identifier.
|
|
274
|
+
results: list[WrappedType] = self.query_models(wrapper_type, primary_identifier, [id_value])
|
|
275
|
+
|
|
276
|
+
# Find the one record, if any, that matches the secondary identifiers.
|
|
277
|
+
unique_record: WrappedType | None = None
|
|
278
|
+
for result in results:
|
|
279
|
+
matches_all: bool = True
|
|
280
|
+
for field, value in secondary_identifiers.items():
|
|
281
|
+
if result.get_field_value(field) != value:
|
|
282
|
+
matches_all = False
|
|
283
|
+
break
|
|
284
|
+
if matches_all:
|
|
285
|
+
# If a previous record in the results already matched all identifiers, then throw an exception.
|
|
286
|
+
if unique_record is not None:
|
|
287
|
+
raise SapioException(f"More than one record of type {wrapper_type.get_wrapper_data_type_name()} "
|
|
288
|
+
f"encountered in system that matches all provided identifiers.")
|
|
289
|
+
unique_record = result
|
|
290
|
+
|
|
465
291
|
# If a unique record matched the identifiers, return it.
|
|
466
292
|
if unique_record is not None:
|
|
467
293
|
return unique_record
|
|
@@ -503,29 +329,6 @@ class RecordHandler:
|
|
|
503
329
|
return_dict[model] = model.get_parents_of_type(parent_type)
|
|
504
330
|
return return_dict
|
|
505
331
|
|
|
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
|
-
|
|
529
332
|
@staticmethod
|
|
530
333
|
def map_by_parents(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
|
|
531
334
|
-> dict[WrappedType, list[RecordModel]]:
|
|
@@ -576,29 +379,6 @@ class RecordHandler:
|
|
|
576
379
|
return_dict[model] = model.get_children_of_type(child_type)
|
|
577
380
|
return return_dict
|
|
578
381
|
|
|
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
|
-
|
|
602
382
|
@staticmethod
|
|
603
383
|
def map_by_children(models: Iterable[RecordModel], child_type: type[WrappedType]) \
|
|
604
384
|
-> dict[WrappedType, list[RecordModel]]:
|
|
@@ -619,7 +399,7 @@ class RecordHandler:
|
|
|
619
399
|
return by_children
|
|
620
400
|
|
|
621
401
|
@staticmethod
|
|
622
|
-
def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name:
|
|
402
|
+
def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name: str,
|
|
623
403
|
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
|
|
624
404
|
"""
|
|
625
405
|
Map a list of record models to their forward side link. The forward side link must already be loaded.
|
|
@@ -630,15 +410,14 @@ class RecordHandler:
|
|
|
630
410
|
:return: A dict[ModelType, SlideLink]. If an input model doesn't have a forward side link of the given type,
|
|
631
411
|
then it will map to None.
|
|
632
412
|
"""
|
|
633
|
-
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
634
413
|
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
635
414
|
for model in models:
|
|
636
415
|
return_dict[model] = model.get_forward_side_link(field_name, side_link_type)
|
|
637
416
|
return return_dict
|
|
638
417
|
|
|
639
418
|
@staticmethod
|
|
640
|
-
def
|
|
641
|
-
|
|
419
|
+
def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: str,
|
|
420
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
642
421
|
"""
|
|
643
422
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
644
423
|
map_to_forward_side_link. Input models that share a forward side link will end up in the same list.
|
|
@@ -650,10 +429,9 @@ class RecordHandler:
|
|
|
650
429
|
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
|
|
651
430
|
pointing to it, then it will not be in the resulting dictionary.
|
|
652
431
|
"""
|
|
653
|
-
|
|
654
|
-
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
432
|
+
to_side_link: dict[RecordModel, WrappedType] = RecordHandler\
|
|
655
433
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
656
|
-
by_side_link: dict[WrappedType, list[
|
|
434
|
+
by_side_link: dict[WrappedType, list[RecordModel]] = {}
|
|
657
435
|
for record, side_link in to_side_link.items():
|
|
658
436
|
if side_link is None:
|
|
659
437
|
continue
|
|
@@ -661,35 +439,8 @@ class RecordHandler:
|
|
|
661
439
|
return by_side_link
|
|
662
440
|
|
|
663
441
|
@staticmethod
|
|
664
|
-
def
|
|
665
|
-
|
|
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]]:
|
|
442
|
+
def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: str,
|
|
443
|
+
side_link_type: type[WrappedType]) -> dict[RecordModel, list[WrappedRecordModel]]:
|
|
693
444
|
"""
|
|
694
445
|
Map a list of record models to a list reverse side links of a given type. The reverse side links must already
|
|
695
446
|
be loaded.
|
|
@@ -701,38 +452,13 @@ class RecordHandler:
|
|
|
701
452
|
:return: A dict[ModelType, list[SideLink]]. If an input model doesn't have reverse side links of the given type,
|
|
702
453
|
then it will map to an empty list.
|
|
703
454
|
"""
|
|
704
|
-
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
705
455
|
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
706
456
|
for model in models:
|
|
707
457
|
return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
|
|
708
458
|
return return_dict
|
|
709
459
|
|
|
710
460
|
@staticmethod
|
|
711
|
-
def
|
|
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,
|
|
461
|
+
def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: str,
|
|
736
462
|
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
737
463
|
"""
|
|
738
464
|
Take a list of record models and map them by their reverse side links. Essentially an inversion of
|
|
@@ -746,43 +472,14 @@ class RecordHandler:
|
|
|
746
472
|
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
|
|
747
473
|
pointing to it, then it will not be in the resulting dictionary.
|
|
748
474
|
"""
|
|
749
|
-
|
|
750
|
-
to_side_links: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler\
|
|
475
|
+
to_side_links: dict[RecordModel, list[WrappedType]] = RecordHandler\
|
|
751
476
|
.map_to_reverse_side_links(models, field_name, side_link_type)
|
|
752
|
-
by_side_links: dict[WrappedType, list[
|
|
477
|
+
by_side_links: dict[WrappedType, list[RecordModel]] = {}
|
|
753
478
|
for record, side_links in to_side_links.items():
|
|
754
479
|
for side_link in side_links:
|
|
755
480
|
by_side_links.setdefault(side_link, []).append(record)
|
|
756
481
|
return by_side_links
|
|
757
482
|
|
|
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
|
-
|
|
786
483
|
@staticmethod
|
|
787
484
|
def map_by_id(models: Iterable[SapioRecord]) -> dict[int, SapioRecord]:
|
|
788
485
|
"""
|
|
@@ -797,8 +494,7 @@ class RecordHandler:
|
|
|
797
494
|
return ret_dict
|
|
798
495
|
|
|
799
496
|
@staticmethod
|
|
800
|
-
def map_by_field(models: Iterable[SapioRecord], field_name:
|
|
801
|
-
-> dict[FieldValue, list[SapioRecord]]:
|
|
497
|
+
def map_by_field(models: Iterable[SapioRecord], field_name: str) -> dict[Any, list[SapioRecord]]:
|
|
802
498
|
"""
|
|
803
499
|
Map the given records by one of their fields. If any two records share the same field value, they'll appear in
|
|
804
500
|
the same value list.
|
|
@@ -807,16 +503,14 @@ class RecordHandler:
|
|
|
807
503
|
:param field_name: The field name to map against.
|
|
808
504
|
:return: A dict mapping field values to the records with that value.
|
|
809
505
|
"""
|
|
810
|
-
|
|
811
|
-
ret_dict: dict[FieldValue, list[SapioRecord]] = {}
|
|
506
|
+
ret_dict: dict[Any, list[SapioRecord]] = {}
|
|
812
507
|
for model in models:
|
|
813
|
-
val:
|
|
508
|
+
val: Any = model.get_field_value(field_name)
|
|
814
509
|
ret_dict.setdefault(val, []).append(model)
|
|
815
510
|
return ret_dict
|
|
816
511
|
|
|
817
512
|
@staticmethod
|
|
818
|
-
def map_by_unique_field(models: Iterable[SapioRecord], field_name:
|
|
819
|
-
-> dict[FieldValue, SapioRecord]:
|
|
513
|
+
def map_by_unique_field(models: Iterable[SapioRecord], field_name: str) -> dict[Any, SapioRecord]:
|
|
820
514
|
"""
|
|
821
515
|
Uniquely map the given records by one of their fields. If any two records share the same field value, throws
|
|
822
516
|
an exception.
|
|
@@ -825,17 +519,16 @@ class RecordHandler:
|
|
|
825
519
|
:param field_name: The field name to map against.
|
|
826
520
|
:return: A dict mapping field values to the record with that value.
|
|
827
521
|
"""
|
|
828
|
-
|
|
829
|
-
ret_dict: dict[FieldValue, SapioRecord] = {}
|
|
522
|
+
ret_dict: dict[Any, SapioRecord] = {}
|
|
830
523
|
for model in models:
|
|
831
|
-
val:
|
|
524
|
+
val: Any = model.get_field_value(field_name)
|
|
832
525
|
if val in ret_dict:
|
|
833
526
|
raise SapioException(f"Value {val} encountered more than once in models list.")
|
|
834
527
|
ret_dict.update({val: model})
|
|
835
528
|
return ret_dict
|
|
836
529
|
|
|
837
530
|
@staticmethod
|
|
838
|
-
def sum_of_field(models: Iterable[SapioRecord], field_name:
|
|
531
|
+
def sum_of_field(models: Iterable[SapioRecord], field_name: str) -> float:
|
|
839
532
|
"""
|
|
840
533
|
Sum up the numeric value of a given field across all input models. Excepts that all given models have a value.
|
|
841
534
|
If the field is an integer field, the value will be converted to a float.
|
|
@@ -844,14 +537,13 @@ class RecordHandler:
|
|
|
844
537
|
:param field_name: The name of the numeric field to sum.
|
|
845
538
|
:return: The sum of the field values for the collection of models.
|
|
846
539
|
"""
|
|
847
|
-
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
848
540
|
field_sum: float = 0
|
|
849
541
|
for model in models:
|
|
850
542
|
field_sum += float(model.get_field_value(field_name))
|
|
851
543
|
return field_sum
|
|
852
544
|
|
|
853
545
|
@staticmethod
|
|
854
|
-
def mean_of_field(models: Iterable[SapioRecord], field_name:
|
|
546
|
+
def mean_of_field(models: Iterable[SapioRecord], field_name: str) -> float:
|
|
855
547
|
"""
|
|
856
548
|
Calculate the mean of the numeric value of a given field across all input models. Excepts that all given models
|
|
857
549
|
have a value. If the field is an integer field, the value will be converted to a float.
|
|
@@ -892,8 +584,8 @@ class RecordHandler:
|
|
|
892
584
|
return oldest
|
|
893
585
|
|
|
894
586
|
@staticmethod
|
|
895
|
-
def values_to_field_maps(field_name:
|
|
896
|
-
|
|
587
|
+
def values_to_field_maps(field_name: str, values: Iterable[Any], existing_fields: list[FieldMap] | None = None) \
|
|
588
|
+
-> list[FieldMap]:
|
|
897
589
|
"""
|
|
898
590
|
Add a list of values for a specific field to a list of dictionaries pairing each value to that field name.
|
|
899
591
|
|
|
@@ -904,8 +596,6 @@ class RecordHandler:
|
|
|
904
596
|
:return: A fields map list that contains the given values mapped by the given field name.
|
|
905
597
|
"""
|
|
906
598
|
# 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)
|
|
909
599
|
if existing_fields:
|
|
910
600
|
values = list(values)
|
|
911
601
|
# The number of new values must match the length of the existing fields list.
|
|
@@ -926,6 +616,8 @@ class RecordHandler:
|
|
|
926
616
|
path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
|
|
927
617
|
relationship path must already be loaded.
|
|
928
618
|
|
|
619
|
+
Currently, the relationship path may only contain parent/child nodes.
|
|
620
|
+
|
|
929
621
|
:param models: A list of record models.
|
|
930
622
|
:param path: The relationship path to follow.
|
|
931
623
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -936,44 +628,15 @@ class RecordHandler:
|
|
|
936
628
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
937
629
|
path: list[RelationshipNode] = path.path
|
|
938
630
|
for model in models:
|
|
939
|
-
current: PyRecordModel
|
|
631
|
+
current: PyRecordModel = model if isinstance(model, PyRecordModel) else model.backing_model
|
|
940
632
|
for node in path:
|
|
941
|
-
|
|
942
|
-
direction: RelationshipNodeType = node.direction
|
|
633
|
+
direction = node.direction
|
|
943
634
|
if current is None:
|
|
944
635
|
break
|
|
945
636
|
if direction == RelationshipNodeType.CHILD:
|
|
946
|
-
current = current.get_child_of_type(
|
|
637
|
+
current = current.get_child_of_type(node.data_type_name)
|
|
947
638
|
elif direction == RelationshipNodeType.PARENT:
|
|
948
|
-
current = current.get_parent_of_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]
|
|
639
|
+
current = current.get_parent_of_type(node.data_type_name)
|
|
977
640
|
else:
|
|
978
641
|
raise SapioException("Unsupported path direction.")
|
|
979
642
|
ret_dict.update({model: self.inst_man.wrap(current, wrapper_type) if current else None})
|
|
@@ -986,6 +649,8 @@ class RecordHandler:
|
|
|
986
649
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
987
650
|
relationship path must already be loaded.
|
|
988
651
|
|
|
652
|
+
Currently, the relationship path may only contain parent/child nodes.
|
|
653
|
+
|
|
989
654
|
:param models: A list of record models.
|
|
990
655
|
:param path: The relationship path to follow.
|
|
991
656
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -1000,23 +665,14 @@ class RecordHandler:
|
|
|
1000
665
|
next_search: set[PyRecordModel] = set()
|
|
1001
666
|
# Exhaust the records at each step in the path, then use those records for the next step.
|
|
1002
667
|
for node in path:
|
|
1003
|
-
|
|
1004
|
-
direction: RelationshipNodeType = node.direction
|
|
668
|
+
direction = node.direction
|
|
1005
669
|
if len(current_search) == 0:
|
|
1006
670
|
break
|
|
1007
671
|
for search in current_search:
|
|
1008
672
|
if direction == RelationshipNodeType.CHILD:
|
|
1009
|
-
next_search.update(search.get_children_of_type(
|
|
673
|
+
next_search.update(search.get_children_of_type(node.data_type_name))
|
|
1010
674
|
elif direction == RelationshipNodeType.PARENT:
|
|
1011
|
-
next_search.update(search.get_parents_of_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))
|
|
675
|
+
next_search.update(search.get_parents_of_type(node.data_type_name))
|
|
1020
676
|
else:
|
|
1021
677
|
raise SapioException("Unsupported path direction.")
|
|
1022
678
|
current_search = next_search
|
|
@@ -1034,9 +690,11 @@ class RecordHandler:
|
|
|
1034
690
|
relationship path must already be loaded.
|
|
1035
691
|
|
|
1036
692
|
The path is "flattened" by only following the first record at each step. Useful for traversing 1-to-Many-to-1
|
|
1037
|
-
relationships (e.g. a sample
|
|
693
|
+
relationships (e.g. a sample with is aliquoted to a number of samples, then those aliquots are pooled back
|
|
1038
694
|
together into a single sample).
|
|
1039
695
|
|
|
696
|
+
Currently, the relationship path may only contain parent/child nodes.
|
|
697
|
+
|
|
1040
698
|
:param models: A list of record models.
|
|
1041
699
|
:param path: The relationship path to follow.
|
|
1042
700
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -1049,64 +707,22 @@ class RecordHandler:
|
|
|
1049
707
|
for model in models:
|
|
1050
708
|
current: list[PyRecordModel] = [model if isinstance(model, PyRecordModel) else model.backing_model]
|
|
1051
709
|
for node in path:
|
|
1052
|
-
|
|
1053
|
-
direction: RelationshipNodeType = node.direction
|
|
710
|
+
direction = node.direction
|
|
1054
711
|
if len(current) == 0:
|
|
1055
712
|
break
|
|
1056
713
|
if direction == RelationshipNodeType.CHILD:
|
|
1057
|
-
current = current[0].get_children_of_type(
|
|
714
|
+
current = current[0].get_children_of_type(node.data_type_name)
|
|
1058
715
|
elif direction == RelationshipNodeType.PARENT:
|
|
1059
|
-
current = current[0].get_parents_of_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)
|
|
716
|
+
current = current[0].get_parents_of_type(node.data_type_name)
|
|
1068
717
|
else:
|
|
1069
718
|
raise SapioException("Unsupported path direction.")
|
|
1070
719
|
ret_dict.update({model: self.inst_man.wrap(current[0], wrapper_type) if current else None})
|
|
1071
720
|
return ret_dict
|
|
1072
721
|
|
|
1073
|
-
def
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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}")
|
|
722
|
+
def __exhaust_query_pages(self, data_type_name: str, field: str, value_list: list[Any],
|
|
723
|
+
paging_criteria: DataRecordPojoPageCriteria | None,
|
|
724
|
+
page_limit: int | None) \
|
|
725
|
+
-> tuple[list[DataRecord], DataRecordPojoPageCriteria | None]:
|
|
726
|
+
pager = QueryDataRecordsAutoPager(data_type_name, field, value_list, self.user, paging_criteria)
|
|
727
|
+
pager.max_page = page_limit
|
|
728
|
+
return pager.get_all_at_once(), pager.next_page_criteria
|