sapiopycommons 2024.8.29a317__py3-none-any.whl → 2024.8.30a320__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 +133 -37
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +60 -0
- sapiopycommons/customreport/custom_report_builder.py +125 -0
- sapiopycommons/customreport/term_builder.py +299 -0
- sapiopycommons/datatype/attachment_util.py +11 -10
- sapiopycommons/eln/experiment_handler.py +209 -48
- sapiopycommons/eln/experiment_report_util.py +33 -129
- sapiopycommons/files/complex_data_loader.py +5 -4
- sapiopycommons/files/file_bridge.py +15 -14
- sapiopycommons/files/file_bridge_handler.py +27 -5
- sapiopycommons/files/file_data_handler.py +2 -5
- sapiopycommons/files/file_util.py +38 -5
- sapiopycommons/files/file_validator.py +26 -11
- sapiopycommons/files/file_writer.py +44 -15
- sapiopycommons/general/aliases.py +147 -3
- sapiopycommons/general/audit_log.py +196 -0
- sapiopycommons/general/custom_report_util.py +34 -32
- sapiopycommons/general/popup_util.py +17 -0
- sapiopycommons/general/sapio_links.py +50 -0
- sapiopycommons/general/time_util.py +40 -0
- sapiopycommons/multimodal/multimodal_data.py +0 -1
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +228 -77
- sapiopycommons/rules/eln_rule_handler.py +34 -25
- sapiopycommons/rules/on_save_rule_handler.py +34 -31
- sapiopycommons/webhook/webhook_handlers.py +90 -26
- sapiopycommons/webhook/webservice_handlers.py +67 -0
- {sapiopycommons-2024.8.29a317.dist-info → sapiopycommons-2024.8.30a320.dist-info}/METADATA +1 -1
- sapiopycommons-2024.8.30a320.dist-info/RECORD +50 -0
- sapiopycommons-2024.8.29a317.dist-info/RECORD +0 -43
- {sapiopycommons-2024.8.29a317.dist-info → sapiopycommons-2024.8.30a320.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.8.29a317.dist-info → sapiopycommons-2024.8.30a320.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from collections.abc import Iterable
|
|
2
|
-
from
|
|
4
|
+
from weakref import WeakValueDictionary
|
|
3
5
|
|
|
4
6
|
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
5
7
|
from sapiopylib.rest.User import SapioUser
|
|
@@ -7,7 +9,7 @@ from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, RawReportTer
|
|
|
7
9
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
8
10
|
from sapiopylib.rest.pojo.DataRecordPaging import DataRecordPojoPageCriteria
|
|
9
11
|
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
10
|
-
from sapiopylib.rest.pojo.
|
|
12
|
+
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
11
13
|
from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
|
|
12
14
|
QueryAllRecordsOfTypeAutoPager
|
|
13
15
|
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
@@ -16,8 +18,10 @@ from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelMana
|
|
|
16
18
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, WrappedRecordModel
|
|
17
19
|
from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
|
|
18
20
|
RelationshipNodeType
|
|
21
|
+
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
19
22
|
|
|
20
|
-
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap
|
|
23
|
+
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
|
|
24
|
+
FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey
|
|
21
25
|
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
22
26
|
from sapiopycommons.general.exceptions import SapioException
|
|
23
27
|
|
|
@@ -32,16 +36,38 @@ class RecordHandler:
|
|
|
32
36
|
rec_man: RecordModelManager
|
|
33
37
|
inst_man: RecordModelInstanceManager
|
|
34
38
|
rel_man: RecordModelRelationshipManager
|
|
39
|
+
an_man: RecordModelAncestorManager
|
|
40
|
+
|
|
41
|
+
__instances: WeakValueDictionary[SapioUser, RecordHandler] = WeakValueDictionary()
|
|
42
|
+
__initialized: bool
|
|
35
43
|
|
|
36
|
-
def
|
|
44
|
+
def __new__(cls, context: UserIdentifier):
|
|
37
45
|
"""
|
|
38
46
|
:param context: The current webhook context or a user object to send requests from.
|
|
39
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):
|
|
57
|
+
"""
|
|
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
|
+
|
|
40
65
|
self.user = context if isinstance(context, SapioUser) else context.user
|
|
41
66
|
self.dr_man = DataRecordManager(self.user)
|
|
42
67
|
self.rec_man = RecordModelManager(self.user)
|
|
43
68
|
self.inst_man = self.rec_man.instance_manager
|
|
44
69
|
self.rel_man = self.rec_man.relationship_manager
|
|
70
|
+
self.an_man = RecordModelAncestorManager(self.rec_man)
|
|
45
71
|
|
|
46
72
|
def wrap_model(self, record: DataRecord, wrapper_type: type[WrappedType]) -> WrappedType:
|
|
47
73
|
"""
|
|
@@ -51,6 +77,7 @@ class RecordHandler:
|
|
|
51
77
|
:param wrapper_type: The record model wrapper to use.
|
|
52
78
|
:return: The record model for the input.
|
|
53
79
|
"""
|
|
80
|
+
self.__verify_data_type([record], wrapper_type)
|
|
54
81
|
return self.inst_man.add_existing_record_of_type(record, wrapper_type)
|
|
55
82
|
|
|
56
83
|
def wrap_models(self, records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> list[WrappedType]:
|
|
@@ -61,10 +88,11 @@ class RecordHandler:
|
|
|
61
88
|
:param wrapper_type: The record model wrapper to use.
|
|
62
89
|
:return: The record models for the input.
|
|
63
90
|
"""
|
|
91
|
+
self.__verify_data_type(records, wrapper_type)
|
|
64
92
|
return self.inst_man.add_existing_records_of_type(list(records), wrapper_type)
|
|
65
93
|
|
|
66
|
-
def query_models(self, wrapper_type: type[WrappedType], field:
|
|
67
|
-
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]:
|
|
68
96
|
"""
|
|
69
97
|
Shorthand for using the data record manager to query for a list of data records by field value
|
|
70
98
|
and then converting the results into a list of record models.
|
|
@@ -72,14 +100,20 @@ class RecordHandler:
|
|
|
72
100
|
:param wrapper_type: The record model wrapper to use.
|
|
73
101
|
:param field: The field to query on.
|
|
74
102
|
:param value_list: The values of the field to query on.
|
|
75
|
-
: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.
|
|
76
106
|
:return: The record models for the queried records.
|
|
77
107
|
"""
|
|
78
|
-
|
|
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]
|
|
79
112
|
|
|
80
|
-
def query_and_map_models(self, wrapper_type: type[WrappedType], field:
|
|
81
|
-
|
|
82
|
-
|
|
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]]:
|
|
83
117
|
"""
|
|
84
118
|
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
85
119
|
map_by_field to turn the returned list into a dictionary mapping field values to records.
|
|
@@ -87,17 +121,21 @@ class RecordHandler:
|
|
|
87
121
|
:param wrapper_type: The record model wrapper to use.
|
|
88
122
|
:param field: The field to query and map on.
|
|
89
123
|
:param value_list: The values of the field to query on.
|
|
90
|
-
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
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.
|
|
91
127
|
:param mapping_field: If provided, use this field to map against instead of the field that was queried on.
|
|
92
128
|
:return: The record models for the queried records mapped by field values to the records with that value.
|
|
93
129
|
"""
|
|
94
130
|
if mapping_field is None:
|
|
95
131
|
mapping_field = field
|
|
96
|
-
return self.map_by_field(self.query_models(wrapper_type, field, value_list, page_limit
|
|
132
|
+
return self.map_by_field(self.query_models(wrapper_type, field, value_list, page_limit, page_size),
|
|
133
|
+
mapping_field)
|
|
97
134
|
|
|
98
|
-
def query_and_unique_map_models(self, wrapper_type: type[WrappedType], field:
|
|
99
|
-
|
|
100
|
-
|
|
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]:
|
|
101
139
|
"""
|
|
102
140
|
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
103
141
|
map_by_unique_field to turn the returned list into a dictionary mapping field values to records.
|
|
@@ -106,15 +144,19 @@ class RecordHandler:
|
|
|
106
144
|
:param wrapper_type: The record model wrapper to use.
|
|
107
145
|
:param field: The field to query and map on.
|
|
108
146
|
:param value_list: The values of the field to query on.
|
|
109
|
-
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
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.
|
|
110
150
|
:param mapping_field: If provided, use this field to map against instead of the field that was queried on.
|
|
111
151
|
:return: The record models for the queried records mapped by field values to the record with that value.
|
|
112
152
|
"""
|
|
113
153
|
if mapping_field is None:
|
|
114
154
|
mapping_field = field
|
|
115
|
-
return self.map_by_unique_field(self.query_models(wrapper_type, field, value_list, page_limit
|
|
155
|
+
return self.map_by_unique_field(self.query_models(wrapper_type, field, value_list, page_limit, page_size),
|
|
156
|
+
mapping_field)
|
|
116
157
|
|
|
117
|
-
def query_models_with_criteria(self, wrapper_type: type[WrappedType], field:
|
|
158
|
+
def query_models_with_criteria(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
|
|
159
|
+
value_list: Iterable[FieldValue],
|
|
118
160
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
119
161
|
page_limit: int | None = None) \
|
|
120
162
|
-> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
|
|
@@ -127,26 +169,33 @@ class RecordHandler:
|
|
|
127
169
|
:param value_list: The values of the field to query on.
|
|
128
170
|
:param paging_criteria: The paging criteria to start the query with.
|
|
129
171
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
130
|
-
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.
|
|
131
174
|
:return: The record models for the queried records and the final paging criteria.
|
|
132
175
|
"""
|
|
133
176
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
177
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
134
178
|
pager = QueryDataRecordsAutoPager(dt, field, list(value_list), self.user, paging_criteria)
|
|
135
179
|
pager.max_page = page_limit
|
|
136
180
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
137
181
|
|
|
138
182
|
def query_models_by_id(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
139
|
-
page_limit: int | None = None) -> list[WrappedType]:
|
|
183
|
+
page_limit: int | None = None, page_size: int | None = None) -> list[WrappedType]:
|
|
140
184
|
"""
|
|
141
185
|
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
142
186
|
and then converting the results into a list of record models.
|
|
143
187
|
|
|
144
188
|
:param wrapper_type: The record model wrapper to use.
|
|
145
189
|
:param ids: The list of record IDs to query.
|
|
146
|
-
: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.
|
|
147
193
|
:return: The record models for the queried records.
|
|
148
194
|
"""
|
|
149
|
-
|
|
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]
|
|
150
199
|
|
|
151
200
|
def query_models_by_id_with_criteria(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
152
201
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
@@ -160,7 +209,8 @@ class RecordHandler:
|
|
|
160
209
|
:param ids: The list of record IDs to query.
|
|
161
210
|
:param paging_criteria: The paging criteria to start the query with.
|
|
162
211
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
163
|
-
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.
|
|
164
214
|
:return: The record models for the queried records and the final paging criteria.
|
|
165
215
|
"""
|
|
166
216
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
@@ -168,16 +218,38 @@ class RecordHandler:
|
|
|
168
218
|
pager.max_page = page_limit
|
|
169
219
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
170
220
|
|
|
171
|
-
def
|
|
221
|
+
def query_models_by_id_and_map(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
222
|
+
page_limit: int | None = None, page_size: int | None = None) \
|
|
223
|
+
-> dict[int, WrappedType]:
|
|
224
|
+
"""
|
|
225
|
+
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
226
|
+
and then converting the results into a dictionary of record ID to the record model for that ID.
|
|
227
|
+
|
|
228
|
+
:param wrapper_type: The record model wrapper to use.
|
|
229
|
+
:param ids: The list of record IDs to query.
|
|
230
|
+
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
231
|
+
only functions if you set a page size or the platform enforces a page size.
|
|
232
|
+
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
233
|
+
:return: The record models for the queried records mapped in a dictionary by their record ID.
|
|
234
|
+
"""
|
|
235
|
+
return {x.record_id: x for x in self.query_models_by_id(wrapper_type, ids, page_limit, page_size)}
|
|
236
|
+
|
|
237
|
+
def query_all_models(self, wrapper_type: type[WrappedType], page_limit: int | None = None,
|
|
238
|
+
page_size: int | None = None) -> list[WrappedType]:
|
|
172
239
|
"""
|
|
173
240
|
Shorthand for using the data record manager to query for all data records of a given type
|
|
174
241
|
and then converting the results into a list of record models.
|
|
175
242
|
|
|
176
243
|
:param wrapper_type: The record model wrapper to use.
|
|
177
|
-
: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.
|
|
178
247
|
:return: The record models for the queried records.
|
|
179
248
|
"""
|
|
180
|
-
|
|
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]
|
|
181
253
|
|
|
182
254
|
def query_all_models_with_criteria(self, wrapper_type: type[WrappedType],
|
|
183
255
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
@@ -190,7 +262,8 @@ class RecordHandler:
|
|
|
190
262
|
:param wrapper_type: The record model wrapper to use.
|
|
191
263
|
:param paging_criteria: The paging criteria to start the query with.
|
|
192
264
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
193
|
-
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.
|
|
194
267
|
:return: The record models for the queried records and the final paging criteria.
|
|
195
268
|
"""
|
|
196
269
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
@@ -200,7 +273,7 @@ class RecordHandler:
|
|
|
200
273
|
|
|
201
274
|
def query_models_by_report(self, wrapper_type: type[WrappedType],
|
|
202
275
|
report_name: str | RawReportTerm | CustomReportCriteria,
|
|
203
|
-
filters: dict[
|
|
276
|
+
filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
|
|
204
277
|
page_limit: int | None = None,
|
|
205
278
|
page_size: int | None = None,
|
|
206
279
|
page_number: int | None = None) -> list[WrappedType]:
|
|
@@ -228,11 +301,11 @@ class RecordHandler:
|
|
|
228
301
|
:return: The record models for the queried records that matched the given report.
|
|
229
302
|
"""
|
|
230
303
|
if isinstance(report_name, str):
|
|
231
|
-
results: list[dict[str,
|
|
232
|
-
|
|
304
|
+
results: list[dict[str, FieldValue]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
|
|
305
|
+
page_limit, page_size, page_number)
|
|
233
306
|
elif isinstance(report_name, RawReportTerm):
|
|
234
|
-
results: list[dict[str,
|
|
235
|
-
|
|
307
|
+
results: list[dict[str, FieldValue]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
|
|
308
|
+
page_limit, page_size, page_number)
|
|
236
309
|
elif isinstance(report_name, CustomReportCriteria):
|
|
237
310
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
238
311
|
# Ensure that the root data type is the one we're looking for.
|
|
@@ -243,8 +316,8 @@ class RecordHandler:
|
|
|
243
316
|
# Enforce that the given custom report has a record ID column.
|
|
244
317
|
if not any([x.data_type_name == dt and x.data_field_name == "RecordId" for x in report_name.column_list]):
|
|
245
318
|
report_name.column_list.append(ReportColumn(dt, "RecordId", FieldType.LONG))
|
|
246
|
-
results: list[dict[str,
|
|
247
|
-
|
|
319
|
+
results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, report_name, filters,
|
|
320
|
+
page_limit, page_size, page_number)
|
|
248
321
|
else:
|
|
249
322
|
raise SapioException("Unrecognized report object.")
|
|
250
323
|
|
|
@@ -273,7 +346,8 @@ class RecordHandler:
|
|
|
273
346
|
"""
|
|
274
347
|
return self.inst_man.add_new_records_of_type(num, wrapper_type)
|
|
275
348
|
|
|
276
|
-
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]:
|
|
277
351
|
"""
|
|
278
352
|
Shorthand for using the instance manager to add new models of the given type, and then initializing all those
|
|
279
353
|
models with the given fields.
|
|
@@ -283,13 +357,14 @@ class RecordHandler:
|
|
|
283
357
|
:return: The newly added record models with the provided fields set. The records will be in the same order as
|
|
284
358
|
the fields in the fields list.
|
|
285
359
|
"""
|
|
360
|
+
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
286
361
|
models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
|
|
287
362
|
for model, field_list in zip(models, fields):
|
|
288
363
|
model.set_field_values(field_list)
|
|
289
364
|
return models
|
|
290
365
|
|
|
291
|
-
def find_or_add_model(self, wrapper_type: type[WrappedType], primary_identifier:
|
|
292
|
-
secondary_identifiers:
|
|
366
|
+
def find_or_add_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
|
|
367
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType:
|
|
293
368
|
"""
|
|
294
369
|
Find a unique record that matches the given field values. If no such records exist, add a record model to the
|
|
295
370
|
cache with the identifying fields set to the desired values. This record will be created in the system when
|
|
@@ -312,6 +387,8 @@ class RecordHandler:
|
|
|
312
387
|
if secondary_identifiers is None:
|
|
313
388
|
secondary_identifiers = {}
|
|
314
389
|
|
|
390
|
+
primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
|
|
391
|
+
secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
|
|
315
392
|
unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
|
|
316
393
|
secondary_identifiers)
|
|
317
394
|
# If a unique record matched the identifiers, return it.
|
|
@@ -338,7 +415,7 @@ class RecordHandler:
|
|
|
338
415
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
339
416
|
return self.wrap_models(self.dr_man.add_data_records(dt, num), wrapper_type)
|
|
340
417
|
|
|
341
|
-
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]) \
|
|
342
419
|
-> list[WrappedType]:
|
|
343
420
|
"""
|
|
344
421
|
Shorthand for creating new records via the data record manager with field data to initialize the records with
|
|
@@ -352,10 +429,12 @@ class RecordHandler:
|
|
|
352
429
|
:return: The newly created record models.
|
|
353
430
|
"""
|
|
354
431
|
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
432
|
+
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
355
433
|
return self.wrap_models(self.dr_man.add_data_records_with_data(dt, fields), wrapper_type)
|
|
356
434
|
|
|
357
|
-
def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier:
|
|
358
|
-
secondary_identifiers:
|
|
435
|
+
def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
|
|
436
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
|
|
437
|
+
-> WrappedType:
|
|
359
438
|
"""
|
|
360
439
|
Find a unique record that matches the given field values. If no such records exist, create one with the
|
|
361
440
|
identifying fields set to the desired values. If more than one record with the identifying values exists,
|
|
@@ -379,6 +458,8 @@ class RecordHandler:
|
|
|
379
458
|
if secondary_identifiers is None:
|
|
380
459
|
secondary_identifiers = {}
|
|
381
460
|
|
|
461
|
+
primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
|
|
462
|
+
secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
|
|
382
463
|
unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
|
|
383
464
|
secondary_identifiers)
|
|
384
465
|
# If a unique record matched the identifiers, return it.
|
|
@@ -497,7 +578,7 @@ class RecordHandler:
|
|
|
497
578
|
|
|
498
579
|
@staticmethod
|
|
499
580
|
def map_by_child(models: Iterable[RecordModel], child_type: type[WrappedType]) \
|
|
500
|
-
-> dict[WrappedType,
|
|
581
|
+
-> dict[WrappedType, RecordModel]:
|
|
501
582
|
"""
|
|
502
583
|
Take a list of record models and map them by their children. Essentially an inversion of map_to_child.
|
|
503
584
|
If two records share the same child, an exception will be thrown. The children must already be loaded.
|
|
@@ -538,7 +619,7 @@ class RecordHandler:
|
|
|
538
619
|
return by_children
|
|
539
620
|
|
|
540
621
|
@staticmethod
|
|
541
|
-
def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name:
|
|
622
|
+
def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
542
623
|
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
|
|
543
624
|
"""
|
|
544
625
|
Map a list of record models to their forward side link. The forward side link must already be loaded.
|
|
@@ -549,13 +630,14 @@ class RecordHandler:
|
|
|
549
630
|
:return: A dict[ModelType, SlideLink]. If an input model doesn't have a forward side link of the given type,
|
|
550
631
|
then it will map to None.
|
|
551
632
|
"""
|
|
633
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
552
634
|
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
553
635
|
for model in models:
|
|
554
636
|
return_dict[model] = model.get_forward_side_link(field_name, side_link_type)
|
|
555
637
|
return return_dict
|
|
556
638
|
|
|
557
639
|
@staticmethod
|
|
558
|
-
def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name:
|
|
640
|
+
def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
559
641
|
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
560
642
|
"""
|
|
561
643
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
@@ -568,6 +650,7 @@ class RecordHandler:
|
|
|
568
650
|
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
|
|
569
651
|
pointing to it, then it will not be in the resulting dictionary.
|
|
570
652
|
"""
|
|
653
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
571
654
|
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
572
655
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
573
656
|
by_side_link: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
@@ -578,7 +661,7 @@ class RecordHandler:
|
|
|
578
661
|
return by_side_link
|
|
579
662
|
|
|
580
663
|
@staticmethod
|
|
581
|
-
def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name:
|
|
664
|
+
def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
582
665
|
side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
|
|
583
666
|
"""
|
|
584
667
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
@@ -591,6 +674,7 @@ class RecordHandler:
|
|
|
591
674
|
:return: A dict[SideLink, ModelType]. If an input model doesn't have a forward side link of the given type
|
|
592
675
|
pointing to it, then it will not be in the resulting dictionary.
|
|
593
676
|
"""
|
|
677
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
594
678
|
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
595
679
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
596
680
|
by_side_link: dict[WrappedType, WrappedRecordModel] = {}
|
|
@@ -604,7 +688,7 @@ class RecordHandler:
|
|
|
604
688
|
return by_side_link
|
|
605
689
|
|
|
606
690
|
@staticmethod
|
|
607
|
-
def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name:
|
|
691
|
+
def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
608
692
|
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, list[WrappedType]]:
|
|
609
693
|
"""
|
|
610
694
|
Map a list of record models to a list reverse side links of a given type. The reverse side links must already
|
|
@@ -617,13 +701,14 @@ class RecordHandler:
|
|
|
617
701
|
:return: A dict[ModelType, list[SideLink]]. If an input model doesn't have reverse side links of the given type,
|
|
618
702
|
then it will map to an empty list.
|
|
619
703
|
"""
|
|
704
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
620
705
|
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
621
706
|
for model in models:
|
|
622
707
|
return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
|
|
623
708
|
return return_dict
|
|
624
709
|
|
|
625
710
|
@staticmethod
|
|
626
|
-
def map_to_reverse_side_link(models: Iterable[WrappedRecordModel], field_name:
|
|
711
|
+
def map_to_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
627
712
|
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
|
|
628
713
|
"""
|
|
629
714
|
Map a list of record models to the reverse side link of a given type. If a given record has more than one
|
|
@@ -636,6 +721,7 @@ class RecordHandler:
|
|
|
636
721
|
:return: A dict[ModelType, SideLink]. If an input model doesn't have reverse side links of the given type,
|
|
637
722
|
then it will map to None.
|
|
638
723
|
"""
|
|
724
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
639
725
|
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
640
726
|
for model in models:
|
|
641
727
|
links: list[WrappedType] = model.get_reverse_side_link(field_name, side_link_type)
|
|
@@ -646,7 +732,7 @@ class RecordHandler:
|
|
|
646
732
|
return return_dict
|
|
647
733
|
|
|
648
734
|
@staticmethod
|
|
649
|
-
def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name:
|
|
735
|
+
def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
650
736
|
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
651
737
|
"""
|
|
652
738
|
Take a list of record models and map them by their reverse side links. Essentially an inversion of
|
|
@@ -660,6 +746,7 @@ class RecordHandler:
|
|
|
660
746
|
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
|
|
661
747
|
pointing to it, then it will not be in the resulting dictionary.
|
|
662
748
|
"""
|
|
749
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
663
750
|
to_side_links: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler\
|
|
664
751
|
.map_to_reverse_side_links(models, field_name, side_link_type)
|
|
665
752
|
by_side_links: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
@@ -669,7 +756,7 @@ class RecordHandler:
|
|
|
669
756
|
return by_side_links
|
|
670
757
|
|
|
671
758
|
@staticmethod
|
|
672
|
-
def map_by_reverse_side_link(models: Iterable[WrappedRecordModel], field_name:
|
|
759
|
+
def map_by_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
673
760
|
side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
|
|
674
761
|
"""
|
|
675
762
|
Take a list of record models and map them by their reverse side link. Essentially an inversion of
|
|
@@ -683,6 +770,7 @@ class RecordHandler:
|
|
|
683
770
|
:return: A dict[SideLink, ModelType]. If an input model doesn't have a reverse side link of the given type
|
|
684
771
|
pointing to it, then it will not be in the resulting dictionary.
|
|
685
772
|
"""
|
|
773
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
686
774
|
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
687
775
|
.map_to_reverse_side_link(models, field_name, side_link_type)
|
|
688
776
|
by_side_link: dict[WrappedType, WrappedRecordModel] = {}
|
|
@@ -709,7 +797,8 @@ class RecordHandler:
|
|
|
709
797
|
return ret_dict
|
|
710
798
|
|
|
711
799
|
@staticmethod
|
|
712
|
-
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]]:
|
|
713
802
|
"""
|
|
714
803
|
Map the given records by one of their fields. If any two records share the same field value, they'll appear in
|
|
715
804
|
the same value list.
|
|
@@ -718,14 +807,16 @@ class RecordHandler:
|
|
|
718
807
|
:param field_name: The field name to map against.
|
|
719
808
|
:return: A dict mapping field values to the records with that value.
|
|
720
809
|
"""
|
|
721
|
-
|
|
810
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
811
|
+
ret_dict: dict[FieldValue, list[SapioRecord]] = {}
|
|
722
812
|
for model in models:
|
|
723
|
-
val:
|
|
813
|
+
val: FieldValue = model.get_field_value(field_name)
|
|
724
814
|
ret_dict.setdefault(val, []).append(model)
|
|
725
815
|
return ret_dict
|
|
726
816
|
|
|
727
817
|
@staticmethod
|
|
728
|
-
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]:
|
|
729
820
|
"""
|
|
730
821
|
Uniquely map the given records by one of their fields. If any two records share the same field value, throws
|
|
731
822
|
an exception.
|
|
@@ -734,16 +825,17 @@ class RecordHandler:
|
|
|
734
825
|
:param field_name: The field name to map against.
|
|
735
826
|
:return: A dict mapping field values to the record with that value.
|
|
736
827
|
"""
|
|
737
|
-
|
|
828
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
829
|
+
ret_dict: dict[FieldValue, SapioRecord] = {}
|
|
738
830
|
for model in models:
|
|
739
|
-
val:
|
|
831
|
+
val: FieldValue = model.get_field_value(field_name)
|
|
740
832
|
if val in ret_dict:
|
|
741
833
|
raise SapioException(f"Value {val} encountered more than once in models list.")
|
|
742
834
|
ret_dict.update({val: model})
|
|
743
835
|
return ret_dict
|
|
744
836
|
|
|
745
837
|
@staticmethod
|
|
746
|
-
def sum_of_field(models: Iterable[SapioRecord], field_name:
|
|
838
|
+
def sum_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
747
839
|
"""
|
|
748
840
|
Sum up the numeric value of a given field across all input models. Excepts that all given models have a value.
|
|
749
841
|
If the field is an integer field, the value will be converted to a float.
|
|
@@ -752,13 +844,14 @@ class RecordHandler:
|
|
|
752
844
|
:param field_name: The name of the numeric field to sum.
|
|
753
845
|
:return: The sum of the field values for the collection of models.
|
|
754
846
|
"""
|
|
847
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
755
848
|
field_sum: float = 0
|
|
756
849
|
for model in models:
|
|
757
850
|
field_sum += float(model.get_field_value(field_name))
|
|
758
851
|
return field_sum
|
|
759
852
|
|
|
760
853
|
@staticmethod
|
|
761
|
-
def mean_of_field(models: Iterable[SapioRecord], field_name:
|
|
854
|
+
def mean_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
762
855
|
"""
|
|
763
856
|
Calculate the mean of the numeric value of a given field across all input models. Excepts that all given models
|
|
764
857
|
have a value. If the field is an integer field, the value will be converted to a float.
|
|
@@ -799,8 +892,8 @@ class RecordHandler:
|
|
|
799
892
|
return oldest
|
|
800
893
|
|
|
801
894
|
@staticmethod
|
|
802
|
-
def values_to_field_maps(field_name:
|
|
803
|
-
|
|
895
|
+
def values_to_field_maps(field_name: FieldIdentifier, values: Iterable[FieldValue],
|
|
896
|
+
existing_fields: list[FieldIdentifier] | None = None) -> list[FieldMap]:
|
|
804
897
|
"""
|
|
805
898
|
Add a list of values for a specific field to a list of dictionaries pairing each value to that field name.
|
|
806
899
|
|
|
@@ -811,6 +904,8 @@ class RecordHandler:
|
|
|
811
904
|
:return: A fields map list that contains the given values mapped by the given field name.
|
|
812
905
|
"""
|
|
813
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)
|
|
814
909
|
if existing_fields:
|
|
815
910
|
values = list(values)
|
|
816
911
|
# The number of new values must match the length of the existing fields list.
|
|
@@ -831,8 +926,6 @@ class RecordHandler:
|
|
|
831
926
|
path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
|
|
832
927
|
relationship path must already be loaded.
|
|
833
928
|
|
|
834
|
-
Currently, the relationship path may only contain parent/child nodes.
|
|
835
|
-
|
|
836
929
|
:param models: A list of record models.
|
|
837
930
|
:param path: The relationship path to follow.
|
|
838
931
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -843,15 +936,44 @@ class RecordHandler:
|
|
|
843
936
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
844
937
|
path: list[RelationshipNode] = path.path
|
|
845
938
|
for model in models:
|
|
846
|
-
current: PyRecordModel = model if isinstance(model, PyRecordModel) else model.backing_model
|
|
939
|
+
current: PyRecordModel | None = model if isinstance(model, PyRecordModel) else model.backing_model
|
|
847
940
|
for node in path:
|
|
848
|
-
|
|
941
|
+
data_type: str = node.data_type_name
|
|
942
|
+
direction: RelationshipNodeType = node.direction
|
|
849
943
|
if current is None:
|
|
850
944
|
break
|
|
851
945
|
if direction == RelationshipNodeType.CHILD:
|
|
852
|
-
current = current.get_child_of_type(
|
|
946
|
+
current = current.get_child_of_type(data_type)
|
|
853
947
|
elif direction == RelationshipNodeType.PARENT:
|
|
854
|
-
current = current.get_parent_of_type(
|
|
948
|
+
current = current.get_parent_of_type(data_type)
|
|
949
|
+
elif direction == RelationshipNodeType.ANCESTOR:
|
|
950
|
+
ancestors: list[PyRecordModel] = list(self.an_man.get_ancestors_of_type(current, data_type))
|
|
951
|
+
if not ancestors:
|
|
952
|
+
current = None
|
|
953
|
+
elif len(ancestors) > 1:
|
|
954
|
+
raise SapioException(f"Hierarchy contains multiple ancestors of type {data_type}.")
|
|
955
|
+
else:
|
|
956
|
+
current = ancestors[0]
|
|
957
|
+
elif direction == RelationshipNodeType.DESCENDANT:
|
|
958
|
+
descendants: list[PyRecordModel] = list(self.an_man.get_descendant_of_type(current, data_type))
|
|
959
|
+
if not descendants:
|
|
960
|
+
current = None
|
|
961
|
+
elif len(descendants) > 1:
|
|
962
|
+
raise SapioException(f"Hierarchy contains multiple descendants of type {data_type}.")
|
|
963
|
+
else:
|
|
964
|
+
current = descendants[0]
|
|
965
|
+
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
966
|
+
current = current.get_forward_side_link(node.data_field_name)
|
|
967
|
+
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
968
|
+
field_name: str = node.data_field_name
|
|
969
|
+
reverse_links: list[PyRecordModel] = current.get_reverse_side_link(field_name, data_type)
|
|
970
|
+
if not reverse_links:
|
|
971
|
+
current = None
|
|
972
|
+
elif len(reverse_links) > 1:
|
|
973
|
+
raise SapioException(f"Hierarchy contains multiple reverse links of type {data_type} on field "
|
|
974
|
+
f"{field_name}.")
|
|
975
|
+
else:
|
|
976
|
+
current = reverse_links[0]
|
|
855
977
|
else:
|
|
856
978
|
raise SapioException("Unsupported path direction.")
|
|
857
979
|
ret_dict.update({model: self.inst_man.wrap(current, wrapper_type) if current else None})
|
|
@@ -864,8 +986,6 @@ class RecordHandler:
|
|
|
864
986
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
865
987
|
relationship path must already be loaded.
|
|
866
988
|
|
|
867
|
-
Currently, the relationship path may only contain parent/child nodes.
|
|
868
|
-
|
|
869
989
|
:param models: A list of record models.
|
|
870
990
|
:param path: The relationship path to follow.
|
|
871
991
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -880,14 +1000,23 @@ class RecordHandler:
|
|
|
880
1000
|
next_search: set[PyRecordModel] = set()
|
|
881
1001
|
# Exhaust the records at each step in the path, then use those records for the next step.
|
|
882
1002
|
for node in path:
|
|
883
|
-
|
|
1003
|
+
data_type: str = node.data_type_name
|
|
1004
|
+
direction: RelationshipNodeType = node.direction
|
|
884
1005
|
if len(current_search) == 0:
|
|
885
1006
|
break
|
|
886
1007
|
for search in current_search:
|
|
887
1008
|
if direction == RelationshipNodeType.CHILD:
|
|
888
|
-
next_search.update(search.get_children_of_type(
|
|
1009
|
+
next_search.update(search.get_children_of_type(data_type))
|
|
889
1010
|
elif direction == RelationshipNodeType.PARENT:
|
|
890
|
-
next_search.update(search.get_parents_of_type(
|
|
1011
|
+
next_search.update(search.get_parents_of_type(data_type))
|
|
1012
|
+
elif direction == RelationshipNodeType.ANCESTOR:
|
|
1013
|
+
next_search.update(self.an_man.get_ancestors_of_type(search, data_type))
|
|
1014
|
+
elif direction == RelationshipNodeType.DESCENDANT:
|
|
1015
|
+
next_search.update(self.an_man.get_descendant_of_type(search, data_type))
|
|
1016
|
+
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
1017
|
+
next_search.add(search.get_forward_side_link(node.data_field_name))
|
|
1018
|
+
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
1019
|
+
next_search.update(search.get_reverse_side_link(node.data_field_name, data_type))
|
|
891
1020
|
else:
|
|
892
1021
|
raise SapioException("Unsupported path direction.")
|
|
893
1022
|
current_search = next_search
|
|
@@ -908,8 +1037,6 @@ class RecordHandler:
|
|
|
908
1037
|
relationships (e.g. a sample which is aliquoted to a number of samples, then those aliquots are pooled back
|
|
909
1038
|
together into a single sample).
|
|
910
1039
|
|
|
911
|
-
Currently, the relationship path may only contain parent/child nodes.
|
|
912
|
-
|
|
913
1040
|
:param models: A list of record models.
|
|
914
1041
|
:param path: The relationship path to follow.
|
|
915
1042
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -922,20 +1049,29 @@ class RecordHandler:
|
|
|
922
1049
|
for model in models:
|
|
923
1050
|
current: list[PyRecordModel] = [model if isinstance(model, PyRecordModel) else model.backing_model]
|
|
924
1051
|
for node in path:
|
|
925
|
-
|
|
1052
|
+
data_type: str = node.data_type_name
|
|
1053
|
+
direction: RelationshipNodeType = node.direction
|
|
926
1054
|
if len(current) == 0:
|
|
927
1055
|
break
|
|
928
1056
|
if direction == RelationshipNodeType.CHILD:
|
|
929
|
-
current = current[0].get_children_of_type(
|
|
1057
|
+
current = current[0].get_children_of_type(data_type)
|
|
930
1058
|
elif direction == RelationshipNodeType.PARENT:
|
|
931
|
-
current = current[0].get_parents_of_type(
|
|
1059
|
+
current = current[0].get_parents_of_type(data_type)
|
|
1060
|
+
elif direction == RelationshipNodeType.ANCESTOR:
|
|
1061
|
+
current = list(self.an_man.get_ancestors_of_type(current[0], data_type))
|
|
1062
|
+
elif direction == RelationshipNodeType.DESCENDANT:
|
|
1063
|
+
current = list(self.an_man.get_descendant_of_type(current[0], data_type))
|
|
1064
|
+
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
1065
|
+
current = [current[0].get_forward_side_link(node.data_field_name)]
|
|
1066
|
+
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
1067
|
+
current = current[0].get_reverse_side_link(node.data_field_name, data_type)
|
|
932
1068
|
else:
|
|
933
1069
|
raise SapioException("Unsupported path direction.")
|
|
934
1070
|
ret_dict.update({model: self.inst_man.wrap(current[0], wrapper_type) if current else None})
|
|
935
1071
|
return ret_dict
|
|
936
1072
|
|
|
937
|
-
def __find_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value:
|
|
938
|
-
secondary_identifiers:
|
|
1073
|
+
def __find_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value: FieldValue,
|
|
1074
|
+
secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType | None:
|
|
939
1075
|
"""
|
|
940
1076
|
Find a record from the system that matches the given field values. The primary identifier and value is used
|
|
941
1077
|
to query for the record, then the secondary identifiers may be optionally provided to further filter the
|
|
@@ -959,3 +1095,18 @@ class RecordHandler:
|
|
|
959
1095
|
f"encountered in system that matches all provided identifiers.")
|
|
960
1096
|
unique_record = result
|
|
961
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}")
|