sapiopycommons 2024.3.18a156__py3-none-any.whl → 2025.1.17a402__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/__init__.py +0 -0
- sapiopycommons/callbacks/callback_util.py +2041 -0
- sapiopycommons/callbacks/field_builder.py +545 -0
- sapiopycommons/chem/IndigoMolecules.py +52 -5
- sapiopycommons/chem/Molecules.py +114 -30
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +60 -0
- sapiopycommons/customreport/custom_report_builder.py +137 -0
- sapiopycommons/customreport/term_builder.py +315 -0
- sapiopycommons/datatype/attachment_util.py +17 -15
- sapiopycommons/datatype/data_fields.py +61 -0
- sapiopycommons/datatype/pseudo_data_types.py +440 -0
- sapiopycommons/eln/experiment_handler.py +390 -90
- sapiopycommons/eln/experiment_report_util.py +649 -0
- sapiopycommons/eln/plate_designer.py +152 -0
- sapiopycommons/files/complex_data_loader.py +31 -0
- sapiopycommons/files/file_bridge.py +153 -25
- sapiopycommons/files/file_bridge_handler.py +555 -0
- sapiopycommons/files/file_data_handler.py +633 -0
- sapiopycommons/files/file_util.py +270 -158
- sapiopycommons/files/file_validator.py +569 -0
- sapiopycommons/files/file_writer.py +377 -0
- 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 +259 -18
- sapiopycommons/general/audit_log.py +185 -0
- sapiopycommons/general/custom_report_util.py +252 -31
- sapiopycommons/general/directive_util.py +86 -0
- sapiopycommons/general/exceptions.py +69 -7
- sapiopycommons/general/popup_util.py +85 -18
- sapiopycommons/general/sapio_links.py +50 -0
- sapiopycommons/general/storage_util.py +148 -0
- sapiopycommons/general/time_util.py +97 -7
- sapiopycommons/multimodal/multimodal.py +146 -0
- sapiopycommons/multimodal/multimodal_data.py +490 -0
- sapiopycommons/processtracking/__init__.py +0 -0
- sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
- sapiopycommons/processtracking/endpoints.py +192 -0
- sapiopycommons/recordmodel/record_handler.py +653 -149
- sapiopycommons/rules/eln_rule_handler.py +89 -8
- sapiopycommons/rules/on_save_rule_handler.py +89 -12
- sapiopycommons/sftpconnect/__init__.py +0 -0
- sapiopycommons/sftpconnect/sftp_builder.py +70 -0
- sapiopycommons/webhook/webhook_context.py +39 -0
- sapiopycommons/webhook/webhook_handlers.py +617 -69
- sapiopycommons/webhook/webservice_handlers.py +317 -0
- {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/METADATA +5 -4
- sapiopycommons-2025.1.17a402.dist-info/RECORD +60 -0
- {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/WHEEL +1 -1
- sapiopycommons-2024.3.18a156.dist-info/RECORD +0 -28
- {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,17 +1,27 @@
|
|
|
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
|
|
7
|
+
from sapiopylib.rest.User import SapioUser
|
|
8
|
+
from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, RawReportTerm, ReportColumn
|
|
5
9
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
6
10
|
from sapiopylib.rest.pojo.DataRecordPaging import DataRecordPojoPageCriteria
|
|
7
|
-
from sapiopylib.rest.pojo.
|
|
11
|
+
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
12
|
+
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
13
|
+
from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
|
|
14
|
+
QueryAllRecordsOfTypeAutoPager
|
|
8
15
|
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
9
16
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager, \
|
|
10
17
|
RecordModelRelationshipManager
|
|
11
|
-
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
12
|
-
from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath,
|
|
18
|
+
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, WrappedRecordModel
|
|
19
|
+
from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
|
|
20
|
+
RelationshipNodeType
|
|
21
|
+
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
13
22
|
|
|
14
|
-
from sapiopycommons.general.aliases import RecordModel, SapioRecord
|
|
23
|
+
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
|
|
24
|
+
FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey
|
|
15
25
|
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
16
26
|
from sapiopycommons.general.exceptions import SapioException
|
|
17
27
|
|
|
@@ -21,83 +31,171 @@ class RecordHandler:
|
|
|
21
31
|
"""
|
|
22
32
|
A collection of shorthand methods for dealing with the various record managers.
|
|
23
33
|
"""
|
|
24
|
-
|
|
34
|
+
user: SapioUser
|
|
25
35
|
dr_man: DataRecordManager
|
|
26
36
|
rec_man: RecordModelManager
|
|
27
37
|
inst_man: RecordModelInstanceManager
|
|
28
38
|
rel_man: RecordModelRelationshipManager
|
|
39
|
+
an_man: RecordModelAncestorManager
|
|
40
|
+
|
|
41
|
+
__instances: WeakValueDictionary[SapioUser, RecordHandler] = WeakValueDictionary()
|
|
42
|
+
__initialized: bool
|
|
29
43
|
|
|
30
|
-
def
|
|
44
|
+
def __new__(cls, context: UserIdentifier):
|
|
31
45
|
"""
|
|
32
|
-
:param context: The current webhook context.
|
|
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
|
+
|
|
56
|
+
def __init__(self, context: UserIdentifier):
|
|
33
57
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
self.
|
|
58
|
+
:param context: The current webhook context or a user object to send requests from.
|
|
59
|
+
"""
|
|
60
|
+
self.user = AliasUtil.to_sapio_user(context)
|
|
61
|
+
if self.__initialized:
|
|
62
|
+
return
|
|
63
|
+
self.__initialized = True
|
|
64
|
+
|
|
65
|
+
self.user = context if isinstance(context, SapioUser) else context.user
|
|
66
|
+
self.dr_man = DataRecordManager(self.user)
|
|
67
|
+
self.rec_man = RecordModelManager(self.user)
|
|
37
68
|
self.inst_man = self.rec_man.instance_manager
|
|
38
69
|
self.rel_man = self.rec_man.relationship_manager
|
|
70
|
+
self.an_man = RecordModelAncestorManager(self.rec_man)
|
|
39
71
|
|
|
40
72
|
def wrap_model(self, record: DataRecord, wrapper_type: type[WrappedType]) -> WrappedType:
|
|
41
73
|
"""
|
|
42
74
|
Shorthand for adding a single data record as a record model.
|
|
75
|
+
|
|
43
76
|
:param record: The data record to wrap.
|
|
44
77
|
:param wrapper_type: The record model wrapper to use.
|
|
45
78
|
:return: The record model for the input.
|
|
46
79
|
"""
|
|
80
|
+
self.__verify_data_type([record], wrapper_type)
|
|
47
81
|
return self.inst_man.add_existing_record_of_type(record, wrapper_type)
|
|
48
82
|
|
|
49
83
|
def wrap_models(self, records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> list[WrappedType]:
|
|
50
84
|
"""
|
|
51
85
|
Shorthand for adding a list of data records as record models.
|
|
86
|
+
|
|
52
87
|
:param records: The data records to wrap.
|
|
53
88
|
:param wrapper_type: The record model wrapper to use.
|
|
54
89
|
:return: The record models for the input.
|
|
55
90
|
"""
|
|
91
|
+
self.__verify_data_type(records, wrapper_type)
|
|
56
92
|
return self.inst_man.add_existing_records_of_type(list(records), wrapper_type)
|
|
57
93
|
|
|
58
|
-
def query_models(self, wrapper_type: type[WrappedType], field:
|
|
59
|
-
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]:
|
|
60
96
|
"""
|
|
61
97
|
Shorthand for using the data record manager to query for a list of data records by field value
|
|
62
98
|
and then converting the results into a list of record models.
|
|
99
|
+
|
|
63
100
|
:param wrapper_type: The record model wrapper to use.
|
|
64
101
|
:param field: The field to query on.
|
|
65
102
|
:param value_list: The values of the field to query on.
|
|
66
|
-
: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.
|
|
67
106
|
:return: The record models for the queried records.
|
|
68
107
|
"""
|
|
69
|
-
|
|
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]
|
|
70
112
|
|
|
71
|
-
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],
|
|
72
160
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
73
161
|
page_limit: int | None = None) \
|
|
74
162
|
-> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
|
|
75
163
|
"""
|
|
76
164
|
Shorthand for using the data record manager to query for a list of data records by field value
|
|
77
165
|
and then converting the results into a list of record models.
|
|
166
|
+
|
|
78
167
|
:param wrapper_type: The record model wrapper to use.
|
|
79
168
|
:param field: The field to query on.
|
|
80
169
|
:param value_list: The values of the field to query on.
|
|
81
170
|
:param paging_criteria: The paging criteria to start the query with.
|
|
82
171
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
83
|
-
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.
|
|
84
174
|
:return: The record models for the queried records and the final paging criteria.
|
|
85
175
|
"""
|
|
86
176
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
87
|
-
|
|
88
|
-
|
|
177
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
178
|
+
pager = QueryDataRecordsAutoPager(dt, field, list(value_list), self.user, paging_criteria)
|
|
179
|
+
pager.max_page = page_limit
|
|
180
|
+
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
89
181
|
|
|
90
182
|
def query_models_by_id(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
91
|
-
page_limit: int | None = None) -> list[WrappedType]:
|
|
183
|
+
page_limit: int | None = None, page_size: int | None = None) -> list[WrappedType]:
|
|
92
184
|
"""
|
|
93
185
|
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
94
186
|
and then converting the results into a list of record models.
|
|
187
|
+
|
|
95
188
|
:param wrapper_type: The record model wrapper to use.
|
|
96
189
|
:param ids: The list of record IDs to query.
|
|
97
|
-
: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.
|
|
98
193
|
:return: The record models for the queried records.
|
|
99
194
|
"""
|
|
100
|
-
|
|
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]
|
|
101
199
|
|
|
102
200
|
def query_models_by_id_with_criteria(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
103
201
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
@@ -106,26 +204,52 @@ class RecordHandler:
|
|
|
106
204
|
"""
|
|
107
205
|
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
108
206
|
and then converting the results into a list of record models.
|
|
207
|
+
|
|
109
208
|
:param wrapper_type: The record model wrapper to use.
|
|
110
209
|
:param ids: The list of record IDs to query.
|
|
111
210
|
:param paging_criteria: The paging criteria to start the query with.
|
|
112
211
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
113
|
-
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.
|
|
114
214
|
:return: The record models for the queried records and the final paging criteria.
|
|
115
215
|
"""
|
|
116
216
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
117
|
-
|
|
118
|
-
|
|
217
|
+
pager = QueryDataRecordByIdListAutoPager(dt, list(ids), self.user, paging_criteria)
|
|
218
|
+
pager.max_page = page_limit
|
|
219
|
+
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
220
|
+
|
|
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.
|
|
119
227
|
|
|
120
|
-
|
|
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]:
|
|
121
239
|
"""
|
|
122
240
|
Shorthand for using the data record manager to query for all data records of a given type
|
|
123
241
|
and then converting the results into a list of record models.
|
|
242
|
+
|
|
124
243
|
:param wrapper_type: The record model wrapper to use.
|
|
125
|
-
: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.
|
|
126
247
|
:return: The record models for the queried records.
|
|
127
248
|
"""
|
|
128
|
-
|
|
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]
|
|
129
253
|
|
|
130
254
|
def query_all_models_with_criteria(self, wrapper_type: type[WrappedType],
|
|
131
255
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
@@ -134,40 +258,79 @@ class RecordHandler:
|
|
|
134
258
|
"""
|
|
135
259
|
Shorthand for using the data record manager to query for all data records of a given type
|
|
136
260
|
and then converting the results into a list of record models.
|
|
261
|
+
|
|
137
262
|
:param wrapper_type: The record model wrapper to use.
|
|
138
263
|
:param paging_criteria: The paging criteria to start the query with.
|
|
139
264
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
140
|
-
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.
|
|
141
267
|
:return: The record models for the queried records and the final paging criteria.
|
|
142
268
|
"""
|
|
143
269
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
144
|
-
|
|
145
|
-
|
|
270
|
+
pager = QueryAllRecordsOfTypeAutoPager(dt, self.user, paging_criteria)
|
|
271
|
+
pager.max_page = page_limit
|
|
272
|
+
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
146
273
|
|
|
147
274
|
def query_models_by_report(self, wrapper_type: type[WrappedType],
|
|
148
|
-
report_name: str,
|
|
149
|
-
filters: dict[
|
|
150
|
-
page_limit: int | None = None
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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]:
|
|
280
|
+
"""
|
|
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.
|
|
283
|
+
|
|
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.
|
|
154
288
|
|
|
155
|
-
Will throw an exception if the given system report does not have a RecordId column.
|
|
156
289
|
:param wrapper_type: The record model wrapper to use.
|
|
157
|
-
: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.
|
|
158
292
|
:param filters: If provided, filter the results of the report using the given mapping of headers to values to
|
|
159
293
|
filter on. This filtering is done before the records are queried.
|
|
160
294
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
161
|
-
:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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. Note that the number of the first page is 0.
|
|
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.
|
|
165
327
|
ids: list[int] = [row["RecordId"] for row in results]
|
|
166
328
|
return self.query_models_by_id(wrapper_type, ids)
|
|
167
329
|
|
|
168
330
|
def add_model(self, wrapper_type: type[WrappedType]) -> WrappedType:
|
|
169
331
|
"""
|
|
170
332
|
Shorthand for using the instance manager to add a new record model of the given type.
|
|
333
|
+
|
|
171
334
|
:param wrapper_type: The record model wrapper to use.
|
|
172
335
|
:return: The newly added record model.
|
|
173
336
|
"""
|
|
@@ -176,32 +339,75 @@ class RecordHandler:
|
|
|
176
339
|
def add_models(self, wrapper_type: type[WrappedType], num: int) -> list[WrappedType]:
|
|
177
340
|
"""
|
|
178
341
|
Shorthand for using the instance manager to add new record models of the given type.
|
|
342
|
+
|
|
179
343
|
:param wrapper_type: The record model wrapper to use.
|
|
180
344
|
:param num: The number of models to create.
|
|
181
345
|
:return: The newly added record models.
|
|
182
346
|
"""
|
|
183
347
|
return self.inst_man.add_new_records_of_type(num, wrapper_type)
|
|
184
348
|
|
|
185
|
-
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]:
|
|
186
351
|
"""
|
|
187
352
|
Shorthand for using the instance manager to add new models of the given type, and then initializing all those
|
|
188
353
|
models with the given fields.
|
|
354
|
+
|
|
189
355
|
:param wrapper_type: The record model wrapper to use.
|
|
190
356
|
:param fields: A list of field maps to initialize the record models with.
|
|
191
357
|
:return: The newly added record models with the provided fields set. The records will be in the same order as
|
|
192
358
|
the fields in the fields list.
|
|
193
359
|
"""
|
|
360
|
+
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
194
361
|
models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
|
|
195
|
-
for model,
|
|
196
|
-
model.set_field_values(
|
|
362
|
+
for model, field_list in zip(models, fields):
|
|
363
|
+
model.set_field_values(field_list)
|
|
197
364
|
return models
|
|
198
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
|
+
|
|
199
404
|
def create_models(self, wrapper_type: type[WrappedType], num: int) -> list[WrappedType]:
|
|
200
405
|
"""
|
|
201
406
|
Shorthand for creating new records via the data record manager and then returning them as wrapped
|
|
202
407
|
record models. Useful in cases where your record model needs to have a valid record ID.
|
|
203
408
|
|
|
204
409
|
Makes a webservice call to create the data records.
|
|
410
|
+
|
|
205
411
|
:param wrapper_type: The record model wrapper to use.
|
|
206
412
|
:param num: The number of new records to create.
|
|
207
413
|
:return: The newly created record models.
|
|
@@ -209,7 +415,7 @@ class RecordHandler:
|
|
|
209
415
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
210
416
|
return self.wrap_models(self.dr_man.add_data_records(dt, num), wrapper_type)
|
|
211
417
|
|
|
212
|
-
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]) \
|
|
213
419
|
-> list[WrappedType]:
|
|
214
420
|
"""
|
|
215
421
|
Shorthand for creating new records via the data record manager with field data to initialize the records with
|
|
@@ -217,15 +423,18 @@ class RecordHandler:
|
|
|
217
423
|
record ID.
|
|
218
424
|
|
|
219
425
|
Makes a webservice call to create the data records.
|
|
426
|
+
|
|
220
427
|
:param wrapper_type: The record model wrapper to use.
|
|
221
428
|
:param fields: The field map list to initialize the new data records with.
|
|
222
429
|
:return: The newly created record models.
|
|
223
430
|
"""
|
|
224
431
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
432
|
+
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
225
433
|
return self.wrap_models(self.dr_man.add_data_records_with_data(dt, fields), wrapper_type)
|
|
226
434
|
|
|
227
|
-
def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier:
|
|
228
|
-
|
|
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:
|
|
229
438
|
"""
|
|
230
439
|
Find a unique record that matches the given field values. If no such records exist, create one with the
|
|
231
440
|
identifying fields set to the desired values. If more than one record with the identifying values exists,
|
|
@@ -236,6 +445,7 @@ class RecordHandler:
|
|
|
236
445
|
|
|
237
446
|
Makes a webservice call to query for the existing record. Makes an additional webservice call if the record
|
|
238
447
|
needs to be created.
|
|
448
|
+
|
|
239
449
|
:param wrapper_type: The record model wrapper to use.
|
|
240
450
|
:param primary_identifier: The data field name of the field to search on.
|
|
241
451
|
:param id_value: The value of the identifying field to search for.
|
|
@@ -248,24 +458,10 @@ class RecordHandler:
|
|
|
248
458
|
if secondary_identifiers is None:
|
|
249
459
|
secondary_identifiers = {}
|
|
250
460
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
unique_record: WrappedType | None = None
|
|
256
|
-
for result in results:
|
|
257
|
-
matches_all: bool = True
|
|
258
|
-
for field, value in secondary_identifiers.items():
|
|
259
|
-
if result.get_field_value(field) != value:
|
|
260
|
-
matches_all = False
|
|
261
|
-
break
|
|
262
|
-
if matches_all:
|
|
263
|
-
# If a previous record in the results already matched all identifiers, then throw an exception.
|
|
264
|
-
if unique_record is not None:
|
|
265
|
-
raise SapioException(f"More than one record of type {wrapper_type.get_wrapper_data_type_name()} "
|
|
266
|
-
f"encountered in system that matches all provided identifiers.")
|
|
267
|
-
unique_record = result
|
|
268
|
-
|
|
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)
|
|
269
465
|
# If a unique record matched the identifiers, return it.
|
|
270
466
|
if unique_record is not None:
|
|
271
467
|
return unique_record
|
|
@@ -277,9 +473,10 @@ class RecordHandler:
|
|
|
277
473
|
return self.create_models_with_data(wrapper_type, [secondary_identifiers])[0]
|
|
278
474
|
|
|
279
475
|
@staticmethod
|
|
280
|
-
def map_to_parent(models: Iterable[RecordModel], parent_type: type[WrappedType]) -> dict:
|
|
476
|
+
def map_to_parent(models: Iterable[RecordModel], parent_type: type[WrappedType]) -> dict[RecordModel, WrappedType]:
|
|
281
477
|
"""
|
|
282
478
|
Map a list of record models to a single parent of a given type. The parents must already be loaded.
|
|
479
|
+
|
|
283
480
|
:param models: A list of record models.
|
|
284
481
|
:param parent_type: The record model wrapper of the parent.
|
|
285
482
|
:return: A dict[ModelType, ParentType]. If an input model doesn't have a parent of the given parent type, then
|
|
@@ -291,9 +488,11 @@ class RecordHandler:
|
|
|
291
488
|
return return_dict
|
|
292
489
|
|
|
293
490
|
@staticmethod
|
|
294
|
-
def map_to_parents(models: Iterable[RecordModel], parent_type: type[WrappedType])
|
|
491
|
+
def map_to_parents(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
|
|
492
|
+
-> dict[RecordModel, list[WrappedType]]:
|
|
295
493
|
"""
|
|
296
494
|
Map a list of record models to a list parents of a given type. The parents must already be loaded.
|
|
495
|
+
|
|
297
496
|
:param models: A list of record models.
|
|
298
497
|
:param parent_type: The record model wrapper of the parents.
|
|
299
498
|
:return: A dict[ModelType, list[ParentType]]. If an input model doesn't have a parent of the given parent type,
|
|
@@ -305,10 +504,35 @@ class RecordHandler:
|
|
|
305
504
|
return return_dict
|
|
306
505
|
|
|
307
506
|
@staticmethod
|
|
308
|
-
def
|
|
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
|
+
@staticmethod
|
|
530
|
+
def map_by_parents(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
|
|
531
|
+
-> dict[WrappedType, list[RecordModel]]:
|
|
309
532
|
"""
|
|
310
533
|
Take a list of record models and map them by their parents. Essentially an inversion of map_to_parents. Input
|
|
311
534
|
models that share a parent will end up in the same list. The parents must already be loaded.
|
|
535
|
+
|
|
312
536
|
:param models: A list of record models.
|
|
313
537
|
:param parent_type: The record model wrapper of the parents.
|
|
314
538
|
:return: A dict[ParentType, list[ModelType]]. If an input model doesn't have a parent of the given parent type,
|
|
@@ -322,9 +546,10 @@ class RecordHandler:
|
|
|
322
546
|
return by_parents
|
|
323
547
|
|
|
324
548
|
@staticmethod
|
|
325
|
-
def map_to_child(models: Iterable[RecordModel], child_type: type[WrappedType]) -> dict:
|
|
549
|
+
def map_to_child(models: Iterable[RecordModel], child_type: type[WrappedType]) -> dict[RecordModel, WrappedType]:
|
|
326
550
|
"""
|
|
327
551
|
Map a list of record models to a single child of a given type. The children must already be loaded.
|
|
552
|
+
|
|
328
553
|
:param models: A list of record models.
|
|
329
554
|
:param child_type: The record model wrapper of the child.
|
|
330
555
|
:return: A dict[ModelType, ChildType]. If an input model doesn't have a child of the given child type, then
|
|
@@ -336,9 +561,11 @@ class RecordHandler:
|
|
|
336
561
|
return return_dict
|
|
337
562
|
|
|
338
563
|
@staticmethod
|
|
339
|
-
def map_to_children(models: Iterable[RecordModel], child_type: type[WrappedType])
|
|
564
|
+
def map_to_children(models: Iterable[RecordModel], child_type: type[WrappedType]) \
|
|
565
|
+
-> dict[RecordModel, list[WrappedType]]:
|
|
340
566
|
"""
|
|
341
567
|
Map a list of record models to a list children of a given type. The children must already be loaded.
|
|
568
|
+
|
|
342
569
|
:param models: A list of record models.
|
|
343
570
|
:param child_type: The record model wrapper of the children.
|
|
344
571
|
:return: A dict[ModelType, list[ChildType]]. If an input model doesn't have children of the given child type,
|
|
@@ -350,13 +577,38 @@ class RecordHandler:
|
|
|
350
577
|
return return_dict
|
|
351
578
|
|
|
352
579
|
@staticmethod
|
|
353
|
-
def
|
|
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
|
+
@staticmethod
|
|
603
|
+
def map_by_children(models: Iterable[RecordModel], child_type: type[WrappedType]) \
|
|
604
|
+
-> dict[WrappedType, list[RecordModel]]:
|
|
354
605
|
"""
|
|
355
606
|
Take a list of record models and map them by their children. Essentially an inversion of map_to_children. Input
|
|
356
607
|
models that share a child will end up in the same list. The children must already be loaded.
|
|
608
|
+
|
|
357
609
|
:param models: A list of record models.
|
|
358
610
|
:param child_type: The record model wrapper of the children.
|
|
359
|
-
:return: A dict[
|
|
611
|
+
:return: A dict[ChildType, list[ModelType]]. If an input model doesn't have children of the given child type,
|
|
360
612
|
then it will not be in the resulting dictionary.
|
|
361
613
|
"""
|
|
362
614
|
to_children: dict[RecordModel, list[WrappedType]] = RecordHandler.map_to_children(models, child_type)
|
|
@@ -366,10 +618,176 @@ class RecordHandler:
|
|
|
366
618
|
by_children.setdefault(child, []).append(record)
|
|
367
619
|
return by_children
|
|
368
620
|
|
|
621
|
+
@staticmethod
|
|
622
|
+
def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
623
|
+
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
|
|
624
|
+
"""
|
|
625
|
+
Map a list of record models to their forward side link. The forward side link must already be loaded.
|
|
626
|
+
|
|
627
|
+
:param models: A list of record models.
|
|
628
|
+
:param field_name: The field name on the record models where the side link is located.
|
|
629
|
+
:param side_link_type: The record model wrapper of the forward side link.
|
|
630
|
+
:return: A dict[ModelType, SlideLink]. If an input model doesn't have a forward side link of the given type,
|
|
631
|
+
then it will map to None.
|
|
632
|
+
"""
|
|
633
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
634
|
+
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
635
|
+
for model in models:
|
|
636
|
+
return_dict[model] = model.get_forward_side_link(field_name, side_link_type)
|
|
637
|
+
return return_dict
|
|
638
|
+
|
|
639
|
+
@staticmethod
|
|
640
|
+
def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
641
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
642
|
+
"""
|
|
643
|
+
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
644
|
+
map_to_forward_side_link. Input models that share a forward side link will end up in the same list.
|
|
645
|
+
The forward side link must already be loaded.
|
|
646
|
+
|
|
647
|
+
:param models: A list of record models.
|
|
648
|
+
:param field_name: The field name on the record models where the side link is located.
|
|
649
|
+
:param side_link_type: The record model wrapper of the forward side links.
|
|
650
|
+
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
|
|
651
|
+
pointing to it, then it will not be in the resulting dictionary.
|
|
652
|
+
"""
|
|
653
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
654
|
+
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
655
|
+
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
656
|
+
by_side_link: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
657
|
+
for record, side_link in to_side_link.items():
|
|
658
|
+
if side_link is None:
|
|
659
|
+
continue
|
|
660
|
+
by_side_link.setdefault(side_link, []).append(record)
|
|
661
|
+
return by_side_link
|
|
662
|
+
|
|
663
|
+
@staticmethod
|
|
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]]:
|
|
693
|
+
"""
|
|
694
|
+
Map a list of record models to a list reverse side links of a given type. The reverse side links must already
|
|
695
|
+
be loaded.
|
|
696
|
+
|
|
697
|
+
:param models: A list of record models.
|
|
698
|
+
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
699
|
+
located.
|
|
700
|
+
:param side_link_type: The record model wrapper of the reverse side links.
|
|
701
|
+
:return: A dict[ModelType, list[SideLink]]. If an input model doesn't have reverse side links of the given type,
|
|
702
|
+
then it will map to an empty list.
|
|
703
|
+
"""
|
|
704
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
705
|
+
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
706
|
+
for model in models:
|
|
707
|
+
return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
|
|
708
|
+
return return_dict
|
|
709
|
+
|
|
710
|
+
@staticmethod
|
|
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,
|
|
736
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
737
|
+
"""
|
|
738
|
+
Take a list of record models and map them by their reverse side links. Essentially an inversion of
|
|
739
|
+
map_to_reverse_side_links. Input models that share a reverse side link will end up in the same list.
|
|
740
|
+
The reverse side links must already be loaded.
|
|
741
|
+
|
|
742
|
+
:param models: A list of record models.
|
|
743
|
+
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
744
|
+
located.
|
|
745
|
+
:param side_link_type: The record model wrapper of the reverse side links.
|
|
746
|
+
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
|
|
747
|
+
pointing to it, then it will not be in the resulting dictionary.
|
|
748
|
+
"""
|
|
749
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
750
|
+
to_side_links: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler\
|
|
751
|
+
.map_to_reverse_side_links(models, field_name, side_link_type)
|
|
752
|
+
by_side_links: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
753
|
+
for record, side_links in to_side_links.items():
|
|
754
|
+
for side_link in side_links:
|
|
755
|
+
by_side_links.setdefault(side_link, []).append(record)
|
|
756
|
+
return by_side_links
|
|
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
|
+
|
|
369
786
|
@staticmethod
|
|
370
787
|
def map_by_id(models: Iterable[SapioRecord]) -> dict[int, SapioRecord]:
|
|
371
788
|
"""
|
|
372
789
|
Map the given records their record IDs.
|
|
790
|
+
|
|
373
791
|
:param models: The records to map.
|
|
374
792
|
:return: A dict mapping the record ID to each record.
|
|
375
793
|
"""
|
|
@@ -379,56 +797,65 @@ class RecordHandler:
|
|
|
379
797
|
return ret_dict
|
|
380
798
|
|
|
381
799
|
@staticmethod
|
|
382
|
-
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]]:
|
|
383
802
|
"""
|
|
384
803
|
Map the given records by one of their fields. If any two records share the same field value, they'll appear in
|
|
385
804
|
the same value list.
|
|
805
|
+
|
|
386
806
|
:param models: The records to map.
|
|
387
807
|
:param field_name: The field name to map against.
|
|
388
808
|
:return: A dict mapping field values to the records with that value.
|
|
389
809
|
"""
|
|
390
|
-
|
|
810
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
811
|
+
ret_dict: dict[FieldValue, list[SapioRecord]] = {}
|
|
391
812
|
for model in models:
|
|
392
|
-
val:
|
|
813
|
+
val: FieldValue = model.get_field_value(field_name)
|
|
393
814
|
ret_dict.setdefault(val, []).append(model)
|
|
394
815
|
return ret_dict
|
|
395
816
|
|
|
396
817
|
@staticmethod
|
|
397
|
-
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]:
|
|
398
820
|
"""
|
|
399
821
|
Uniquely map the given records by one of their fields. If any two records share the same field value, throws
|
|
400
822
|
an exception.
|
|
823
|
+
|
|
401
824
|
:param models: The records to map.
|
|
402
825
|
:param field_name: The field name to map against.
|
|
403
826
|
:return: A dict mapping field values to the record with that value.
|
|
404
827
|
"""
|
|
405
|
-
|
|
828
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
829
|
+
ret_dict: dict[FieldValue, SapioRecord] = {}
|
|
406
830
|
for model in models:
|
|
407
|
-
val:
|
|
831
|
+
val: FieldValue = model.get_field_value(field_name)
|
|
408
832
|
if val in ret_dict:
|
|
409
833
|
raise SapioException(f"Value {val} encountered more than once in models list.")
|
|
410
834
|
ret_dict.update({val: model})
|
|
411
835
|
return ret_dict
|
|
412
836
|
|
|
413
837
|
@staticmethod
|
|
414
|
-
def sum_of_field(models: Iterable[SapioRecord], field_name:
|
|
838
|
+
def sum_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
415
839
|
"""
|
|
416
840
|
Sum up the numeric value of a given field across all input models. Excepts that all given models have a value.
|
|
417
841
|
If the field is an integer field, the value will be converted to a float.
|
|
842
|
+
|
|
418
843
|
:param models: The models to calculate the sum of.
|
|
419
844
|
:param field_name: The name of the numeric field to sum.
|
|
420
845
|
:return: The sum of the field values for the collection of models.
|
|
421
846
|
"""
|
|
847
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
422
848
|
field_sum: float = 0
|
|
423
849
|
for model in models:
|
|
424
850
|
field_sum += float(model.get_field_value(field_name))
|
|
425
851
|
return field_sum
|
|
426
852
|
|
|
427
853
|
@staticmethod
|
|
428
|
-
def mean_of_field(models: Iterable[SapioRecord], field_name:
|
|
854
|
+
def mean_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
429
855
|
"""
|
|
430
856
|
Calculate the mean of the numeric value of a given field across all input models. Excepts that all given models
|
|
431
857
|
have a value. If the field is an integer field, the value will be converted to a float.
|
|
858
|
+
|
|
432
859
|
:param models: The models to calculate the mean of.
|
|
433
860
|
:param field_name: The name of the numeric field to mean.
|
|
434
861
|
:return: The mean of the field values for the collection of models.
|
|
@@ -439,6 +866,7 @@ class RecordHandler:
|
|
|
439
866
|
def get_newest_record(records: Iterable[SapioRecord]) -> SapioRecord:
|
|
440
867
|
"""
|
|
441
868
|
Get the newest record from a list of records.
|
|
869
|
+
|
|
442
870
|
:param records: The list of records.
|
|
443
871
|
:return: The input record with the highest record ID. None if the input list is empty.
|
|
444
872
|
"""
|
|
@@ -448,11 +876,27 @@ class RecordHandler:
|
|
|
448
876
|
newest = record
|
|
449
877
|
return newest
|
|
450
878
|
|
|
879
|
+
# FR-46696: Add a function for getting the oldest record in a list, just like we have one for the newest record.
|
|
880
|
+
@staticmethod
|
|
881
|
+
def get_oldest_record(records: Iterable[SapioRecord]) -> SapioRecord:
|
|
882
|
+
"""
|
|
883
|
+
Get the oldest record from a list of records.
|
|
884
|
+
|
|
885
|
+
:param records: The list of records.
|
|
886
|
+
:return: The input record with the lowest record ID. None if the input list is empty.
|
|
887
|
+
"""
|
|
888
|
+
oldest: SapioRecord | None = None
|
|
889
|
+
for record in records:
|
|
890
|
+
if oldest is None or record.record_id < oldest.record_id:
|
|
891
|
+
oldest = record
|
|
892
|
+
return oldest
|
|
893
|
+
|
|
451
894
|
@staticmethod
|
|
452
|
-
def values_to_field_maps(field_name:
|
|
453
|
-
|
|
895
|
+
def values_to_field_maps(field_name: FieldIdentifier, values: Iterable[FieldValue],
|
|
896
|
+
existing_fields: list[FieldIdentifier] | None = None) -> list[FieldMap]:
|
|
454
897
|
"""
|
|
455
898
|
Add a list of values for a specific field to a list of dictionaries pairing each value to that field name.
|
|
899
|
+
|
|
456
900
|
:param field_name: The name of the field that the values are from.
|
|
457
901
|
:param values: A list of field values.
|
|
458
902
|
:param existing_fields: An optional existing fields map list to add the new values to. Values are added in the
|
|
@@ -460,6 +904,8 @@ class RecordHandler:
|
|
|
460
904
|
:return: A fields map list that contains the given values mapped by the given field name.
|
|
461
905
|
"""
|
|
462
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)
|
|
463
909
|
if existing_fields:
|
|
464
910
|
values = list(values)
|
|
465
911
|
# The number of new values must match the length of the existing fields list.
|
|
@@ -471,7 +917,7 @@ class RecordHandler:
|
|
|
471
917
|
# Otherwise, create a new fields map list.
|
|
472
918
|
return [{field_name: value} for value in values]
|
|
473
919
|
|
|
474
|
-
# FR-46155: Update relationship path traversing functions to be non-
|
|
920
|
+
# FR-46155: Update relationship path traversing functions to be non-static and take in a wrapper type so that the
|
|
475
921
|
# output can be wrapped instead of requiring the user to wrap the output.
|
|
476
922
|
def get_linear_path(self, models: Iterable[RecordModel], path: RelationshipPath, wrapper_type: type[WrappedType]) \
|
|
477
923
|
-> dict[RecordModel, WrappedType | None]:
|
|
@@ -479,6 +925,7 @@ class RecordHandler:
|
|
|
479
925
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
480
926
|
path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
|
|
481
927
|
relationship path must already be loaded.
|
|
928
|
+
|
|
482
929
|
:param models: A list of record models.
|
|
483
930
|
:param path: The relationship path to follow.
|
|
484
931
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -486,16 +933,49 @@ class RecordHandler:
|
|
|
486
933
|
path couldn't be reached, the record will map to None.
|
|
487
934
|
"""
|
|
488
935
|
ret_dict: dict[RecordModel, WrappedType | None] = {}
|
|
489
|
-
|
|
936
|
+
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
937
|
+
path: list[RelationshipNode] = path.path
|
|
490
938
|
for model in models:
|
|
491
|
-
current: PyRecordModel = model if isinstance(model, PyRecordModel) else model.backing_model
|
|
492
|
-
for
|
|
939
|
+
current: PyRecordModel | None = model if isinstance(model, PyRecordModel) else model.backing_model
|
|
940
|
+
for node in path:
|
|
941
|
+
data_type: str = node.data_type_name
|
|
942
|
+
direction: RelationshipNodeType = node.direction
|
|
493
943
|
if current is None:
|
|
494
944
|
break
|
|
495
|
-
if direction ==
|
|
496
|
-
current = current.get_child_of_type(
|
|
497
|
-
elif direction ==
|
|
498
|
-
current = current.get_parent_of_type(
|
|
945
|
+
if direction == RelationshipNodeType.CHILD:
|
|
946
|
+
current = current.get_child_of_type(data_type)
|
|
947
|
+
elif direction == RelationshipNodeType.PARENT:
|
|
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(data_type, field_name)
|
|
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]
|
|
977
|
+
else:
|
|
978
|
+
raise SapioException("Unsupported path direction.")
|
|
499
979
|
ret_dict.update({model: self.inst_man.wrap(current, wrapper_type) if current else None})
|
|
500
980
|
return ret_dict
|
|
501
981
|
|
|
@@ -505,6 +985,7 @@ class RecordHandler:
|
|
|
505
985
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
506
986
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
507
987
|
relationship path must already be loaded.
|
|
988
|
+
|
|
508
989
|
:param models: A list of record models.
|
|
509
990
|
:param path: The relationship path to follow.
|
|
510
991
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -512,19 +993,32 @@ class RecordHandler:
|
|
|
512
993
|
path couldn't be reached, the record will map to an empty list.
|
|
513
994
|
"""
|
|
514
995
|
ret_dict: dict[RecordModel, list[WrappedType]] = {}
|
|
515
|
-
|
|
996
|
+
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
997
|
+
path: list[RelationshipNode] = path.path
|
|
516
998
|
for model in models:
|
|
517
999
|
current_search: set[PyRecordModel] = {model if isinstance(model, PyRecordModel) else model.backing_model}
|
|
518
1000
|
next_search: set[PyRecordModel] = set()
|
|
519
1001
|
# Exhaust the records at each step in the path, then use those records for the next step.
|
|
520
|
-
for
|
|
1002
|
+
for node in path:
|
|
1003
|
+
data_type: str = node.data_type_name
|
|
1004
|
+
direction: RelationshipNodeType = node.direction
|
|
521
1005
|
if len(current_search) == 0:
|
|
522
1006
|
break
|
|
523
1007
|
for search in current_search:
|
|
524
|
-
if direction ==
|
|
525
|
-
next_search.update(search.get_children_of_type(
|
|
526
|
-
elif direction ==
|
|
527
|
-
next_search.update(search.get_parents_of_type(
|
|
1008
|
+
if direction == RelationshipNodeType.CHILD:
|
|
1009
|
+
next_search.update(search.get_children_of_type(data_type))
|
|
1010
|
+
elif direction == RelationshipNodeType.PARENT:
|
|
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(data_type, node.data_field_name))
|
|
1020
|
+
else:
|
|
1021
|
+
raise SapioException("Unsupported path direction.")
|
|
528
1022
|
current_search = next_search
|
|
529
1023
|
next_search = set()
|
|
530
1024
|
ret_dict.update({model: self.inst_man.wrap_list(list(current_search), wrapper_type)})
|
|
@@ -540,8 +1034,9 @@ class RecordHandler:
|
|
|
540
1034
|
relationship path must already be loaded.
|
|
541
1035
|
|
|
542
1036
|
The path is "flattened" by only following the first record at each step. Useful for traversing 1-to-Many-to-1
|
|
543
|
-
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
|
|
544
1038
|
together into a single sample).
|
|
1039
|
+
|
|
545
1040
|
:param models: A list of record models.
|
|
546
1041
|
:param path: The relationship path to follow.
|
|
547
1042
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -549,60 +1044,69 @@ class RecordHandler:
|
|
|
549
1044
|
path couldn't be reached, the record will map to None.
|
|
550
1045
|
"""
|
|
551
1046
|
ret_dict: dict[RecordModel, WrappedType | None] = {}
|
|
552
|
-
|
|
1047
|
+
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
1048
|
+
path: list[RelationshipNode] = path.path
|
|
553
1049
|
for model in models:
|
|
554
1050
|
current: list[PyRecordModel] = [model if isinstance(model, PyRecordModel) else model.backing_model]
|
|
555
|
-
for
|
|
1051
|
+
for node in path:
|
|
1052
|
+
data_type: str = node.data_type_name
|
|
1053
|
+
direction: RelationshipNodeType = node.direction
|
|
556
1054
|
if len(current) == 0:
|
|
557
1055
|
break
|
|
558
|
-
if direction ==
|
|
559
|
-
current = current[0].get_children_of_type(
|
|
560
|
-
elif direction ==
|
|
561
|
-
current = current[0].get_parents_of_type(
|
|
1056
|
+
if direction == RelationshipNodeType.CHILD:
|
|
1057
|
+
current = current[0].get_children_of_type(data_type)
|
|
1058
|
+
elif direction == RelationshipNodeType.PARENT:
|
|
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(data_type, node.data_field_name)
|
|
1068
|
+
else:
|
|
1069
|
+
raise SapioException("Unsupported path direction.")
|
|
562
1070
|
ret_dict.update({model: self.inst_man.wrap(current[0], wrapper_type) if current else None})
|
|
563
1071
|
return ret_dict
|
|
564
1072
|
|
|
565
|
-
def
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
has_next_page = page_result.is_next_page_available
|
|
606
|
-
records.extend(page_result.result_list)
|
|
607
|
-
cur_page += 1
|
|
608
|
-
return records, paging_criteria
|
|
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}")
|