sapiopycommons 2025.4.9a150__py3-none-any.whl → 2025.4.9a476__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 +1262 -392
- sapiopycommons/callbacks/field_builder.py +2 -0
- sapiopycommons/chem/Molecules.py +0 -2
- sapiopycommons/customreport/auto_pagers.py +281 -0
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/attachment_util.py +4 -2
- sapiopycommons/datatype/data_fields.py +23 -1
- sapiopycommons/eln/experiment_cache.py +173 -0
- sapiopycommons/eln/experiment_handler.py +933 -279
- sapiopycommons/eln/experiment_report_util.py +15 -10
- sapiopycommons/eln/experiment_step_factory.py +474 -0
- sapiopycommons/eln/experiment_tags.py +7 -0
- sapiopycommons/eln/plate_designer.py +159 -59
- sapiopycommons/eln/step_creation.py +235 -0
- sapiopycommons/files/file_bridge.py +76 -0
- sapiopycommons/files/file_bridge_handler.py +325 -110
- sapiopycommons/files/file_data_handler.py +2 -2
- sapiopycommons/files/file_util.py +40 -15
- sapiopycommons/files/file_validator.py +6 -5
- sapiopycommons/files/file_writer.py +1 -1
- sapiopycommons/flowcyto/flow_cyto.py +1 -1
- sapiopycommons/general/accession_service.py +3 -3
- sapiopycommons/general/aliases.py +51 -28
- sapiopycommons/general/audit_log.py +2 -2
- sapiopycommons/general/custom_report_util.py +24 -1
- sapiopycommons/general/data_structure_util.py +115 -0
- sapiopycommons/general/directive_util.py +86 -0
- sapiopycommons/general/exceptions.py +41 -2
- sapiopycommons/general/popup_util.py +2 -2
- sapiopycommons/multimodal/multimodal.py +1 -0
- sapiopycommons/processtracking/custom_workflow_handler.py +46 -30
- sapiopycommons/recordmodel/record_handler.py +547 -159
- sapiopycommons/rules/eln_rule_handler.py +41 -30
- sapiopycommons/rules/on_save_rule_handler.py +41 -30
- sapiopycommons/samples/aliquot.py +48 -0
- sapiopycommons/webhook/webhook_handlers.py +448 -55
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.4.9a150.dist-info → sapiopycommons-2025.4.9a476.dist-info}/METADATA +1 -1
- sapiopycommons-2025.4.9a476.dist-info/RECORD +67 -0
- sapiopycommons-2025.4.9a150.dist-info/RECORD +0 -59
- {sapiopycommons-2025.4.9a150.dist-info → sapiopycommons-2025.4.9a476.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.4.9a150.dist-info → sapiopycommons-2025.4.9a476.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import io
|
|
4
|
+
import warnings
|
|
3
5
|
from collections.abc import Iterable
|
|
6
|
+
from typing import Collection
|
|
4
7
|
from weakref import WeakValueDictionary
|
|
5
8
|
|
|
6
9
|
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
@@ -12,19 +15,28 @@ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
|
12
15
|
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
13
16
|
from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
|
|
14
17
|
QueryAllRecordsOfTypeAutoPager
|
|
15
|
-
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
18
|
+
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel, AbstractRecordModelPropertyGetter, \
|
|
19
|
+
RecordModelPropertyType, AbstractRecordModelPropertyAdder, AbstractRecordModelPropertySetter, \
|
|
20
|
+
AbstractRecordModelPropertyRemover
|
|
16
21
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager, \
|
|
17
22
|
RecordModelRelationshipManager
|
|
18
23
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, WrappedRecordModel
|
|
19
24
|
from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
|
|
20
25
|
RelationshipNodeType
|
|
21
26
|
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
27
|
+
from sapiopylib.rest.utils.recordmodel.properties import Parents, Parent, Children, Child, ForwardSideLink
|
|
22
28
|
|
|
23
29
|
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
|
|
24
|
-
FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey
|
|
30
|
+
FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey, DataTypeIdentifier
|
|
25
31
|
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
26
32
|
from sapiopycommons.general.exceptions import SapioException
|
|
27
33
|
|
|
34
|
+
# Aliases for longer name.
|
|
35
|
+
_PropertyGetter = AbstractRecordModelPropertyGetter
|
|
36
|
+
_PropertyAdder = AbstractRecordModelPropertyAdder
|
|
37
|
+
_PropertyRemover = AbstractRecordModelPropertyRemover
|
|
38
|
+
_PropertySetter = AbstractRecordModelPropertySetter
|
|
39
|
+
_PropertyType = RecordModelPropertyType
|
|
28
40
|
|
|
29
41
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
30
42
|
class RecordHandler:
|
|
@@ -57,226 +69,273 @@ class RecordHandler:
|
|
|
57
69
|
"""
|
|
58
70
|
:param context: The current webhook context or a user object to send requests from.
|
|
59
71
|
"""
|
|
60
|
-
self.user = AliasUtil.to_sapio_user(context)
|
|
61
72
|
if self.__initialized:
|
|
62
73
|
return
|
|
63
74
|
self.__initialized = True
|
|
64
75
|
|
|
65
|
-
self.user =
|
|
76
|
+
self.user = AliasUtil.to_sapio_user(context)
|
|
66
77
|
self.dr_man = DataRecordManager(self.user)
|
|
67
78
|
self.rec_man = RecordModelManager(self.user)
|
|
68
79
|
self.inst_man = self.rec_man.instance_manager
|
|
69
80
|
self.rel_man = self.rec_man.relationship_manager
|
|
70
81
|
self.an_man = RecordModelAncestorManager(self.rec_man)
|
|
71
82
|
|
|
72
|
-
|
|
83
|
+
# CR-47491: Support not providing a wrapper type to receive PyRecordModels instead of WrappedRecordModels.
|
|
84
|
+
def wrap_model(self, record: DataRecord | PyRecordModel, wrapper_type: type[WrappedType] | None = None) \
|
|
85
|
+
-> WrappedType | PyRecordModel:
|
|
73
86
|
"""
|
|
74
|
-
Shorthand for adding a single data record as a
|
|
87
|
+
Shorthand for adding a single data record or PyRecordModel as a WrappedRecordModel.
|
|
75
88
|
|
|
76
|
-
:param record: The data record to wrap.
|
|
77
|
-
:param wrapper_type: The record model wrapper to use.
|
|
89
|
+
:param record: The data record or PyRecordModel to wrap.
|
|
90
|
+
:param wrapper_type: The record model wrapper to use. If not provided, the record is returned as a
|
|
91
|
+
PyRecordModel instead of a WrappedRecordModel.
|
|
78
92
|
:return: The record model for the input.
|
|
79
93
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
if wrapper_type is not None:
|
|
95
|
+
self.__verify_data_type(record, wrapper_type)
|
|
96
|
+
if isinstance(record, PyRecordModel):
|
|
97
|
+
return self.inst_man.wrap(record, wrapper_type)
|
|
98
|
+
return self.inst_man.add_existing_record_of_type(record, wrapper_type)
|
|
99
|
+
if isinstance(record, PyRecordModel):
|
|
100
|
+
return record
|
|
101
|
+
return self.inst_man.add_existing_record(record)
|
|
82
102
|
|
|
83
|
-
def wrap_models(self, records: Iterable[DataRecord
|
|
103
|
+
def wrap_models(self, records: Iterable[DataRecord | PyRecordModel],
|
|
104
|
+
wrapper_type: type[WrappedType] | None = None) \
|
|
105
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
84
106
|
"""
|
|
85
|
-
Shorthand for adding a list of data records as
|
|
107
|
+
Shorthand for adding a list of data records or PyRecordModels as a WrappedRecordModels.
|
|
86
108
|
|
|
87
109
|
:param records: The data records to wrap.
|
|
88
|
-
:param wrapper_type: The record model wrapper to use.
|
|
110
|
+
:param wrapper_type: The record model wrapper to use. If not provided, the records are returned as
|
|
111
|
+
PyRecordModels instead of WrappedRecordModels.
|
|
89
112
|
:return: The record models for the input.
|
|
90
113
|
"""
|
|
91
|
-
self.
|
|
92
|
-
return self.inst_man.add_existing_records_of_type(list(records), wrapper_type)
|
|
114
|
+
return [self.wrap_model(x, wrapper_type) for x in records]
|
|
93
115
|
|
|
94
|
-
|
|
95
|
-
|
|
116
|
+
# CR-47491: Support providing a data type name string to receive PyRecordModels instead of requiring a WrapperType.
|
|
117
|
+
# CR-47523: Support a singular field value being provided for the value_list parameter.
|
|
118
|
+
def query_models(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
|
|
119
|
+
value_list: Iterable[FieldValue] | FieldValue,
|
|
120
|
+
page_limit: int | None = None, page_size: int | None = None) \
|
|
121
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
96
122
|
"""
|
|
97
123
|
Shorthand for using the data record manager to query for a list of data records by field value
|
|
98
124
|
and then converting the results into a list of record models.
|
|
99
125
|
|
|
100
|
-
:param wrapper_type: The record model wrapper to use.
|
|
126
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
101
127
|
:param field: The field to query on.
|
|
102
|
-
:param value_list: The values of the field to query on
|
|
128
|
+
:param value_list: The values of the field to query on, or a singular field value that will be automatically
|
|
129
|
+
converted to a singleton list. Note that field values of None are not supported by this method and will be
|
|
130
|
+
ignored. If you need to query for records with a null field value, use a custom report.
|
|
103
131
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
104
132
|
only functions if you set a page size or the platform enforces a page size.
|
|
105
133
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
106
|
-
:return: The record models for the queried records.
|
|
134
|
+
:return: The record models for the queried records. If a data type name was used instead of a model wrapper,
|
|
135
|
+
then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
107
136
|
"""
|
|
108
137
|
criteria: DataRecordPojoPageCriteria | None = None
|
|
109
138
|
if page_size is not None:
|
|
110
139
|
criteria = DataRecordPojoPageCriteria(page_size=page_size)
|
|
111
140
|
return self.query_models_with_criteria(wrapper_type, field, value_list, criteria, page_limit)[0]
|
|
112
141
|
|
|
113
|
-
def query_and_map_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
|
|
114
|
-
value_list: Iterable[FieldValue]
|
|
115
|
-
|
|
116
|
-
|
|
142
|
+
def query_and_map_models(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
|
|
143
|
+
value_list: Iterable[FieldValue] | FieldValue,
|
|
144
|
+
page_limit: int | None = None, page_size: int | None = None,
|
|
145
|
+
*,
|
|
146
|
+
mapping_field: FieldIdentifier | None = None) \
|
|
147
|
+
-> dict[FieldValue, list[WrappedType] | list[PyRecordModel]]:
|
|
117
148
|
"""
|
|
118
149
|
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
119
150
|
map_by_field to turn the returned list into a dictionary mapping field values to records.
|
|
120
151
|
|
|
121
|
-
:param wrapper_type: The record model wrapper to use.
|
|
152
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
122
153
|
:param field: The field to query and map on.
|
|
123
|
-
:param value_list: The values of the field to query on
|
|
154
|
+
:param value_list: The values of the field to query on, or a singular field value that will be automatically
|
|
155
|
+
converted to a singleton list. Note that field values of None are not supported by this method and will be
|
|
156
|
+
ignored. If you need to query for records with a null field value, use a custom report.
|
|
124
157
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
125
158
|
only functions if you set a page size or the platform enforces a page size.
|
|
126
159
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
127
160
|
:param mapping_field: If provided, use this field to map against instead of the field that was queried on.
|
|
128
161
|
:return: The record models for the queried records mapped by field values to the records with that value.
|
|
162
|
+
If a data type name was used instead of a model wrapper, then the returned records will be PyRecordModels
|
|
163
|
+
instead of WrappedRecordModels.
|
|
129
164
|
"""
|
|
130
165
|
if mapping_field is None:
|
|
131
166
|
mapping_field = field
|
|
132
167
|
return self.map_by_field(self.query_models(wrapper_type, field, value_list, page_limit, page_size),
|
|
133
168
|
mapping_field)
|
|
134
169
|
|
|
135
|
-
def query_and_unique_map_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
|
|
136
|
-
value_list: Iterable[FieldValue]
|
|
137
|
-
|
|
138
|
-
|
|
170
|
+
def query_and_unique_map_models(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
|
|
171
|
+
value_list: Iterable[FieldValue] | FieldValue,
|
|
172
|
+
page_limit: int | None = None, page_size: int | None = None,
|
|
173
|
+
*,
|
|
174
|
+
mapping_field: FieldIdentifier | None = None) \
|
|
175
|
+
-> dict[FieldValue, WrappedType | PyRecordModel]:
|
|
139
176
|
"""
|
|
140
177
|
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
141
178
|
map_by_unique_field to turn the returned list into a dictionary mapping field values to records.
|
|
142
179
|
If any two records share the same field value, throws an exception.
|
|
143
180
|
|
|
144
|
-
:param wrapper_type: The record model wrapper to use.
|
|
181
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
145
182
|
:param field: The field to query and map on.
|
|
146
|
-
:param value_list: The values of the field to query on
|
|
183
|
+
:param value_list: The values of the field to query on, or a singular field value that will be automatically
|
|
184
|
+
converted to a singleton list. Note that field values of None are not supported by this method and will be
|
|
185
|
+
ignored. If you need to query for records with a null field value, use a custom report.
|
|
147
186
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
148
187
|
only functions if you set a page size or the platform enforces a page size.
|
|
149
188
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
150
189
|
:param mapping_field: If provided, use this field to map against instead of the field that was queried on.
|
|
151
190
|
:return: The record models for the queried records mapped by field values to the record with that value.
|
|
191
|
+
If a data type name was used instead of a model wrapper, then the returned records will be PyRecordModels
|
|
192
|
+
instead of WrappedRecordModels.
|
|
152
193
|
"""
|
|
153
194
|
if mapping_field is None:
|
|
154
195
|
mapping_field = field
|
|
155
196
|
return self.map_by_unique_field(self.query_models(wrapper_type, field, value_list, page_limit, page_size),
|
|
156
197
|
mapping_field)
|
|
157
198
|
|
|
158
|
-
def query_models_with_criteria(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
|
|
159
|
-
value_list: Iterable[FieldValue],
|
|
199
|
+
def query_models_with_criteria(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
|
|
200
|
+
value_list: Iterable[FieldValue] | FieldValue,
|
|
160
201
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
161
202
|
page_limit: int | None = None) \
|
|
162
|
-
-> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
|
|
203
|
+
-> tuple[list[WrappedType] | list[PyRecordModel], DataRecordPojoPageCriteria]:
|
|
163
204
|
"""
|
|
164
205
|
Shorthand for using the data record manager to query for a list of data records by field value
|
|
165
206
|
and then converting the results into a list of record models.
|
|
166
207
|
|
|
167
|
-
:param wrapper_type: The record model wrapper to use.
|
|
208
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
168
209
|
:param field: The field to query on.
|
|
169
|
-
:param value_list: The values of the field to query on
|
|
210
|
+
:param value_list: The values of the field to query on, or a singular field value that will be automatically
|
|
211
|
+
converted to a singleton list. Note that field values of None are not supported by this method and will be
|
|
212
|
+
ignored. If you need to query for records with a null field value, use a custom report.
|
|
170
213
|
:param paging_criteria: The paging criteria to start the query with.
|
|
171
214
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
172
215
|
possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
|
|
173
216
|
enforces a page size.
|
|
174
|
-
:return: The record models for the queried records and the final paging criteria.
|
|
217
|
+
:return: The record models for the queried records and the final paging criteria. If a data type name was used
|
|
218
|
+
instead of a model wrapper, then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
175
219
|
"""
|
|
176
|
-
dt: str =
|
|
220
|
+
dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
221
|
+
if isinstance(wrapper_type, str):
|
|
222
|
+
wrapper_type = None
|
|
177
223
|
field: str = AliasUtil.to_data_field_name(field)
|
|
224
|
+
if isinstance(value_list, FieldValue):
|
|
225
|
+
value_list: list[FieldValue] = [value_list]
|
|
178
226
|
pager = QueryDataRecordsAutoPager(dt, field, list(value_list), self.user, paging_criteria)
|
|
179
227
|
pager.max_page = page_limit
|
|
180
228
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
181
229
|
|
|
182
|
-
def query_models_by_id(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
183
|
-
page_limit: int | None = None, page_size: int | None = None)
|
|
230
|
+
def query_models_by_id(self, wrapper_type: type[WrappedType] | str, ids: Iterable[int],
|
|
231
|
+
page_limit: int | None = None, page_size: int | None = None) \
|
|
232
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
184
233
|
"""
|
|
185
234
|
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
186
235
|
and then converting the results into a list of record models.
|
|
187
236
|
|
|
188
|
-
:param wrapper_type: The record model wrapper to use.
|
|
237
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
189
238
|
:param ids: The list of record IDs to query.
|
|
190
239
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
191
240
|
only functions if you set a page size or the platform enforces a page size.
|
|
192
241
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
193
|
-
:return: The record models for the queried records.
|
|
242
|
+
:return: The record models for the queried records. If a data type name was used instead of a model wrapper,
|
|
243
|
+
then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
194
244
|
"""
|
|
195
245
|
criteria: DataRecordPojoPageCriteria | None = None
|
|
196
246
|
if page_size is not None:
|
|
197
247
|
criteria = DataRecordPojoPageCriteria(page_size=page_size)
|
|
198
248
|
return self.query_models_by_id_with_criteria(wrapper_type, ids, criteria, page_limit)[0]
|
|
199
249
|
|
|
200
|
-
def query_models_by_id_with_criteria(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
250
|
+
def query_models_by_id_with_criteria(self, wrapper_type: type[WrappedType] | str, ids: Iterable[int],
|
|
201
251
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
202
252
|
page_limit: int | None = None) \
|
|
203
|
-
-> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
|
|
253
|
+
-> tuple[list[WrappedType] | list[PyRecordModel], DataRecordPojoPageCriteria]:
|
|
204
254
|
"""
|
|
205
255
|
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
206
256
|
and then converting the results into a list of record models.
|
|
207
257
|
|
|
208
|
-
:param wrapper_type: The record model wrapper to use.
|
|
258
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
209
259
|
:param ids: The list of record IDs to query.
|
|
210
260
|
:param paging_criteria: The paging criteria to start the query with.
|
|
211
261
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
212
262
|
possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
|
|
213
263
|
enforces a page size.
|
|
214
|
-
:return: The record models for the queried records and the final paging criteria.
|
|
264
|
+
:return: The record models for the queried records and the final paging criteria. If a data type name was used
|
|
265
|
+
instead of a model wrapper, then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
215
266
|
"""
|
|
216
|
-
dt: str =
|
|
267
|
+
dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
268
|
+
if isinstance(wrapper_type, str):
|
|
269
|
+
wrapper_type = None
|
|
217
270
|
pager = QueryDataRecordByIdListAutoPager(dt, list(ids), self.user, paging_criteria)
|
|
218
271
|
pager.max_page = page_limit
|
|
219
272
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
220
273
|
|
|
221
|
-
def query_models_by_id_and_map(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
274
|
+
def query_models_by_id_and_map(self, wrapper_type: type[WrappedType] | str, ids: Iterable[int],
|
|
222
275
|
page_limit: int | None = None, page_size: int | None = None) \
|
|
223
|
-
-> dict[int, WrappedType]:
|
|
276
|
+
-> dict[int, WrappedType | PyRecordModel]:
|
|
224
277
|
"""
|
|
225
278
|
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
226
279
|
and then converting the results into a dictionary of record ID to the record model for that ID.
|
|
227
280
|
|
|
228
|
-
:param wrapper_type: The record model wrapper to use.
|
|
281
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
229
282
|
:param ids: The list of record IDs to query.
|
|
230
283
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
231
284
|
only functions if you set a page size or the platform enforces a page size.
|
|
232
285
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
233
286
|
:return: The record models for the queried records mapped in a dictionary by their record ID.
|
|
287
|
+
If a data type name was used instead of a model wrapper, then the returned records will be PyRecordModels
|
|
288
|
+
instead of WrappedRecordModels.
|
|
234
289
|
"""
|
|
235
|
-
return {x
|
|
290
|
+
return {AliasUtil.to_record_id(x): x for x in self.query_models_by_id(wrapper_type, ids, page_limit, page_size)}
|
|
236
291
|
|
|
237
|
-
def query_all_models(self, wrapper_type: type[WrappedType], page_limit: int | None = None,
|
|
238
|
-
page_size: int | None = None) -> list[WrappedType]:
|
|
292
|
+
def query_all_models(self, wrapper_type: type[WrappedType] | str, page_limit: int | None = None,
|
|
293
|
+
page_size: int | None = None) -> list[WrappedType] | list[PyRecordModel]:
|
|
239
294
|
"""
|
|
240
295
|
Shorthand for using the data record manager to query for all data records of a given type
|
|
241
296
|
and then converting the results into a list of record models.
|
|
242
297
|
|
|
243
|
-
:param wrapper_type: The record model wrapper to use.
|
|
298
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
244
299
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
245
300
|
only functions if you set a page size or the platform enforces a page size.
|
|
246
301
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
247
|
-
:return: The record models for the queried records.
|
|
302
|
+
:return: The record models for the queried records. If a data type name was used instead of a model wrapper,
|
|
303
|
+
then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
248
304
|
"""
|
|
249
305
|
criteria: DataRecordPojoPageCriteria | None = None
|
|
250
306
|
if page_size is not None:
|
|
251
307
|
criteria = DataRecordPojoPageCriteria(page_size=page_size)
|
|
252
308
|
return self.query_all_models_with_criteria(wrapper_type, criteria, page_limit)[0]
|
|
253
309
|
|
|
254
|
-
def query_all_models_with_criteria(self, wrapper_type: type[WrappedType],
|
|
310
|
+
def query_all_models_with_criteria(self, wrapper_type: type[WrappedType] | str,
|
|
255
311
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
256
312
|
page_limit: int | None = None) \
|
|
257
|
-
-> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
|
|
313
|
+
-> tuple[list[WrappedType] | list[PyRecordModel], DataRecordPojoPageCriteria]:
|
|
258
314
|
"""
|
|
259
315
|
Shorthand for using the data record manager to query for all data records of a given type
|
|
260
316
|
and then converting the results into a list of record models.
|
|
261
317
|
|
|
262
|
-
:param wrapper_type: The record model wrapper to use.
|
|
318
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
263
319
|
:param paging_criteria: The paging criteria to start the query with.
|
|
264
320
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
265
321
|
possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
|
|
266
322
|
enforces a page size.
|
|
267
|
-
:return: The record models for the queried records and the final paging criteria.
|
|
323
|
+
:return: The record models for the queried records and the final paging criteria. If a data type name was used
|
|
324
|
+
instead of a model wrapper, then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
268
325
|
"""
|
|
269
|
-
dt: str =
|
|
326
|
+
dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
327
|
+
if isinstance(wrapper_type, str):
|
|
328
|
+
wrapper_type = None
|
|
270
329
|
pager = QueryAllRecordsOfTypeAutoPager(dt, self.user, paging_criteria)
|
|
271
330
|
pager.max_page = page_limit
|
|
272
331
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
273
332
|
|
|
274
|
-
def query_models_by_report(self, wrapper_type: type[WrappedType],
|
|
333
|
+
def query_models_by_report(self, wrapper_type: type[WrappedType] | str,
|
|
275
334
|
report_name: str | RawReportTerm | CustomReportCriteria,
|
|
276
335
|
filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
|
|
277
336
|
page_limit: int | None = None,
|
|
278
337
|
page_size: int | None = None,
|
|
279
|
-
page_number: int | None = None) -> list[WrappedType]:
|
|
338
|
+
page_number: int | None = None) -> list[WrappedType] | list[PyRecordModel]:
|
|
280
339
|
"""
|
|
281
340
|
Run a report and use the results of that report to query for and return the records in the report results.
|
|
282
341
|
First runs the report, then runs a data record manager query on the results of the custom report.
|
|
@@ -286,7 +345,7 @@ class RecordHandler:
|
|
|
286
345
|
|
|
287
346
|
Any given custom report criteria should only have columns from a single data type.
|
|
288
347
|
|
|
289
|
-
:param wrapper_type: The record model wrapper to use.
|
|
348
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
290
349
|
:param report_name: The name of a system report, or a raw report term for a quick report, or custom report
|
|
291
350
|
criteria for a custom report.
|
|
292
351
|
:param filters: If provided, filter the results of the report using the given mapping of headers to values to
|
|
@@ -298,8 +357,10 @@ class RecordHandler:
|
|
|
298
357
|
:param page_number: The page number to start the search from, If None, starts on the first page.
|
|
299
358
|
If the input report is a custom report criteria, uses the value from the criteria, unless this value is
|
|
300
359
|
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.
|
|
360
|
+
:return: The record models for the queried records that matched the given report. If a data type name was used
|
|
361
|
+
instead of a model wrapper, then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
302
362
|
"""
|
|
363
|
+
warnings.warn("Deprecated in favor of the [System/Custom/Quick]ReportRecordAutoPager classes.", DeprecationWarning)
|
|
303
364
|
if isinstance(report_name, str):
|
|
304
365
|
results: list[dict[str, FieldValue]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
|
|
305
366
|
page_limit, page_size, page_number)
|
|
@@ -307,7 +368,7 @@ class RecordHandler:
|
|
|
307
368
|
results: list[dict[str, FieldValue]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
|
|
308
369
|
page_limit, page_size, page_number)
|
|
309
370
|
elif isinstance(report_name, CustomReportCriteria):
|
|
310
|
-
dt: str =
|
|
371
|
+
dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
311
372
|
# Ensure that the root data type is the one we're looking for.
|
|
312
373
|
report_name.root_data_type = dt
|
|
313
374
|
# Raise an exception if any column in the report doesn't match the given data type.
|
|
@@ -322,40 +383,45 @@ class RecordHandler:
|
|
|
322
383
|
raise SapioException("Unrecognized report object.")
|
|
323
384
|
|
|
324
385
|
# 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
|
|
386
|
+
# This should only possibly be the case with system reports, as quick reports will include the record ID, and
|
|
326
387
|
# we forced any given custom report to have a record ID column.
|
|
327
388
|
ids: list[int] = [row["RecordId"] for row in results]
|
|
328
389
|
return self.query_models_by_id(wrapper_type, ids)
|
|
329
390
|
|
|
330
|
-
def add_model(self, wrapper_type: type[WrappedType]) -> WrappedType:
|
|
391
|
+
def add_model(self, wrapper_type: type[WrappedType] | str) -> WrappedType | PyRecordModel:
|
|
331
392
|
"""
|
|
332
393
|
Shorthand for using the instance manager to add a new record model of the given type.
|
|
333
394
|
|
|
334
|
-
:param wrapper_type: The record model wrapper to use.
|
|
335
|
-
:return: The newly added record model.
|
|
395
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the record.
|
|
396
|
+
:return: The newly added record model. If a data type name was used instead of a model wrapper, then the
|
|
397
|
+
returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
336
398
|
"""
|
|
337
|
-
return self.
|
|
399
|
+
return self.add_models(wrapper_type, 1)[0]
|
|
338
400
|
|
|
339
|
-
def add_models(self, wrapper_type: type[WrappedType], num: int) -> list[WrappedType]:
|
|
401
|
+
def add_models(self, wrapper_type: type[WrappedType] | str, num: int) -> list[WrappedType] | list[PyRecordModel]:
|
|
340
402
|
"""
|
|
341
403
|
Shorthand for using the instance manager to add new record models of the given type.
|
|
342
404
|
|
|
343
|
-
:param wrapper_type: The record model wrapper to use.
|
|
405
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
344
406
|
:param num: The number of models to create.
|
|
345
|
-
:return: The newly added record models.
|
|
407
|
+
:return: The newly added record models. If a data type name was used instead of a model wrapper, then the
|
|
408
|
+
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
346
409
|
"""
|
|
410
|
+
if isinstance(wrapper_type, str):
|
|
411
|
+
return self.inst_man.add_new_records(wrapper_type, num)
|
|
347
412
|
return self.inst_man.add_new_records_of_type(num, wrapper_type)
|
|
348
413
|
|
|
349
|
-
def add_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldIdentifierMap]) \
|
|
350
|
-
-> list[WrappedType]:
|
|
414
|
+
def add_models_with_data(self, wrapper_type: type[WrappedType] | str, fields: list[FieldIdentifierMap]) \
|
|
415
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
351
416
|
"""
|
|
352
417
|
Shorthand for using the instance manager to add new models of the given type, and then initializing all those
|
|
353
418
|
models with the given fields.
|
|
354
419
|
|
|
355
|
-
:param wrapper_type: The record model wrapper to use.
|
|
420
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
356
421
|
:param fields: A list of field maps to initialize the record models with.
|
|
357
422
|
:return: The newly added record models with the provided fields set. The records will be in the same order as
|
|
358
|
-
the fields in the fields list.
|
|
423
|
+
the fields in the fields list. If a data type name was used instead of a model wrapper, then the returned
|
|
424
|
+
records will be PyRecordModels instead of WrappedRecordModels.
|
|
359
425
|
"""
|
|
360
426
|
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
361
427
|
models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
|
|
@@ -363,8 +429,9 @@ class RecordHandler:
|
|
|
363
429
|
model.set_field_values(field_list)
|
|
364
430
|
return models
|
|
365
431
|
|
|
366
|
-
def find_or_add_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
|
|
367
|
-
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None)
|
|
432
|
+
def find_or_add_model(self, wrapper_type: type[WrappedType] | str, primary_identifier: FieldIdentifier,
|
|
433
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
|
|
434
|
+
-> WrappedType | PyRecordModel:
|
|
368
435
|
"""
|
|
369
436
|
Find a unique record that matches the given field values. If no such records exist, add a record model to the
|
|
370
437
|
cache with the identifying fields set to the desired values. This record will be created in the system when
|
|
@@ -375,12 +442,14 @@ class RecordHandler:
|
|
|
375
442
|
|
|
376
443
|
Makes a webservice call to query for the existing record.
|
|
377
444
|
|
|
378
|
-
:param wrapper_type: The record model wrapper to use.
|
|
445
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the record.
|
|
379
446
|
:param primary_identifier: The data field name of the field to search on.
|
|
380
447
|
:param id_value: The value of the identifying field to search for.
|
|
381
448
|
:param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
|
|
382
449
|
the primary identifier.
|
|
383
450
|
:return: The record model with the identifying field value, either pulled from the system or newly created.
|
|
451
|
+
If a data type name was used instead of a model wrapper, then the returned record will be a PyRecordModel
|
|
452
|
+
instead of a WrappedRecordModel.
|
|
384
453
|
"""
|
|
385
454
|
# PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
|
|
386
455
|
# If no secondary identifiers were provided, use an empty dictionary.
|
|
@@ -401,22 +470,25 @@ class RecordHandler:
|
|
|
401
470
|
secondary_identifiers.update({primary_identifier: id_value})
|
|
402
471
|
return self.add_models_with_data(wrapper_type, [secondary_identifiers])[0]
|
|
403
472
|
|
|
404
|
-
def create_models(self, wrapper_type: type[WrappedType], num: int) -> list[WrappedType]:
|
|
473
|
+
def create_models(self, wrapper_type: type[WrappedType] | str, num: int) -> list[WrappedType] | list[PyRecordModel]:
|
|
405
474
|
"""
|
|
406
475
|
Shorthand for creating new records via the data record manager and then returning them as wrapped
|
|
407
476
|
record models. Useful in cases where your record model needs to have a valid record ID.
|
|
408
477
|
|
|
409
478
|
Makes a webservice call to create the data records.
|
|
410
479
|
|
|
411
|
-
:param wrapper_type: The record model wrapper to use.
|
|
480
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
412
481
|
:param num: The number of new records to create.
|
|
413
|
-
:return: The newly created record models.
|
|
482
|
+
:return: The newly created record models. If a data type name was used instead of a model wrapper, then the
|
|
483
|
+
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
414
484
|
"""
|
|
415
|
-
dt: str =
|
|
485
|
+
dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
486
|
+
if isinstance(wrapper_type, str):
|
|
487
|
+
wrapper_type = None
|
|
416
488
|
return self.wrap_models(self.dr_man.add_data_records(dt, num), wrapper_type)
|
|
417
489
|
|
|
418
|
-
def create_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldIdentifierMap]) \
|
|
419
|
-
-> list[WrappedType]:
|
|
490
|
+
def create_models_with_data(self, wrapper_type: type[WrappedType] | str, fields: list[FieldIdentifierMap]) \
|
|
491
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
420
492
|
"""
|
|
421
493
|
Shorthand for creating new records via the data record manager with field data to initialize the records with
|
|
422
494
|
and then returning them as wrapped record models. Useful in cases where your record model needs to have a valid
|
|
@@ -424,17 +496,20 @@ class RecordHandler:
|
|
|
424
496
|
|
|
425
497
|
Makes a webservice call to create the data records.
|
|
426
498
|
|
|
427
|
-
:param wrapper_type: The record model wrapper to use.
|
|
499
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
428
500
|
:param fields: The field map list to initialize the new data records with.
|
|
429
|
-
:return: The newly created record models.
|
|
501
|
+
:return: The newly created record models. If a data type name was used instead of a model wrapper, then the
|
|
502
|
+
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
430
503
|
"""
|
|
431
|
-
dt: str =
|
|
504
|
+
dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
505
|
+
if isinstance(wrapper_type, str):
|
|
506
|
+
wrapper_type = None
|
|
432
507
|
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
433
508
|
return self.wrap_models(self.dr_man.add_data_records_with_data(dt, fields), wrapper_type)
|
|
434
509
|
|
|
435
|
-
def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
|
|
510
|
+
def find_or_create_model(self, wrapper_type: type[WrappedType] | str, primary_identifier: FieldIdentifier,
|
|
436
511
|
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
|
|
437
|
-
-> WrappedType:
|
|
512
|
+
-> WrappedType | PyRecordModel:
|
|
438
513
|
"""
|
|
439
514
|
Find a unique record that matches the given field values. If no such records exist, create one with the
|
|
440
515
|
identifying fields set to the desired values. If more than one record with the identifying values exists,
|
|
@@ -446,12 +521,14 @@ class RecordHandler:
|
|
|
446
521
|
Makes a webservice call to query for the existing record. Makes an additional webservice call if the record
|
|
447
522
|
needs to be created.
|
|
448
523
|
|
|
449
|
-
:param wrapper_type: The record model wrapper to use.
|
|
524
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the record.
|
|
450
525
|
:param primary_identifier: The data field name of the field to search on.
|
|
451
526
|
:param id_value: The value of the identifying field to search for.
|
|
452
527
|
:param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
|
|
453
528
|
the primary identifier.
|
|
454
529
|
:return: The record model with the identifying field value, either pulled from the system or newly created.
|
|
530
|
+
If a data type name was used instead of a model wrapper, then the returned record will be a PyRecordModel
|
|
531
|
+
instead of a WrappedRecordModel.
|
|
455
532
|
"""
|
|
456
533
|
# PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
|
|
457
534
|
# If no secondary identifiers were provided, use an empty dictionary.
|
|
@@ -472,8 +549,281 @@ class RecordHandler:
|
|
|
472
549
|
secondary_identifiers.update({primary_identifier: id_value})
|
|
473
550
|
return self.create_models_with_data(wrapper_type, [secondary_identifiers])[0]
|
|
474
551
|
|
|
552
|
+
# FR-47525: Add functions for getting and setting record image bytes.
|
|
553
|
+
def get_record_image(self, record: SapioRecord) -> bytes:
|
|
554
|
+
"""
|
|
555
|
+
Retrieve the record image for a given record.
|
|
556
|
+
|
|
557
|
+
:param record: The record model to retrieve the image of.
|
|
558
|
+
:return: The file bytes of the given record's image.
|
|
559
|
+
"""
|
|
560
|
+
record: DataRecord = AliasUtil.to_data_record(record)
|
|
561
|
+
with io.BytesIO() as data_sink:
|
|
562
|
+
def consume_data(chunk: bytes):
|
|
563
|
+
data_sink.write(chunk)
|
|
564
|
+
|
|
565
|
+
self.dr_man.get_record_image(record, consume_data)
|
|
566
|
+
data_sink.flush()
|
|
567
|
+
data_sink.seek(0)
|
|
568
|
+
file_bytes = data_sink.read()
|
|
569
|
+
return file_bytes
|
|
570
|
+
|
|
571
|
+
def set_record_image(self, record: SapioRecord, file_data: str | bytes) -> None:
|
|
572
|
+
"""
|
|
573
|
+
Set the record image for a given record.
|
|
574
|
+
|
|
575
|
+
:param record: The record model to set the image of.
|
|
576
|
+
:param file_data: The file data of the image to set on the record.
|
|
577
|
+
"""
|
|
578
|
+
record: DataRecord = AliasUtil.to_data_record(record)
|
|
579
|
+
with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as stream:
|
|
580
|
+
self.dr_man.set_record_image(record, stream)
|
|
581
|
+
|
|
582
|
+
# FR-47522: Add RecordHandler functions that copy from the RecordModelUtil class in our Java utilities.
|
|
583
|
+
@staticmethod
|
|
584
|
+
def get_values_list(records: list[RecordModel], field: FieldIdentifier) -> list[FieldValue]:
|
|
585
|
+
"""
|
|
586
|
+
Get a list of field values from a list of record models.
|
|
587
|
+
|
|
588
|
+
:param records: The list of record models to get the field values from.
|
|
589
|
+
:param field: The field to get the values of.
|
|
590
|
+
:return: A list of field values from the input record models. The values are in the same order as the input
|
|
591
|
+
record models.
|
|
592
|
+
"""
|
|
593
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
594
|
+
return [x.get_field_value(field) for x in records]
|
|
595
|
+
|
|
596
|
+
@staticmethod
|
|
597
|
+
def get_values_set(records: list[RecordModel], field: FieldIdentifier) -> set[FieldValue]:
|
|
598
|
+
"""
|
|
599
|
+
Get a set of field values from a list of record models.
|
|
600
|
+
|
|
601
|
+
:param records: The list of record models to get the field values from.
|
|
602
|
+
:param field: The field to get the values of.
|
|
603
|
+
:return: A set of field values from the input record models.
|
|
604
|
+
"""
|
|
605
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
606
|
+
return {x.get_field_value(field) for x in records}
|
|
607
|
+
|
|
608
|
+
@staticmethod
|
|
609
|
+
def set_values(records: list[RecordModel], field: FieldIdentifier, value: FieldValue) -> None:
|
|
610
|
+
"""
|
|
611
|
+
Set the value of a field on a list of record models.
|
|
612
|
+
|
|
613
|
+
:param records: The list of record models to set the field value on.
|
|
614
|
+
:param field: The field to set the value of.
|
|
615
|
+
:param value: The value to set the field to for all input records.
|
|
616
|
+
"""
|
|
617
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
618
|
+
for record in records:
|
|
619
|
+
record.set_field_value(field, value)
|
|
620
|
+
|
|
621
|
+
@staticmethod
|
|
622
|
+
def get_min_record(records: list[RecordModel], field: FieldIdentifier) -> RecordModel:
|
|
623
|
+
"""
|
|
624
|
+
Get the record model with the minimum value of a given field from a list of record models.
|
|
625
|
+
|
|
626
|
+
:param records: The list of record models to search through.
|
|
627
|
+
:param field: The field to find the minimum value of.
|
|
628
|
+
:return: The record model with the minimum value of the given field.
|
|
629
|
+
"""
|
|
630
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
631
|
+
return min(records, key=lambda x: x.get_field_value(field))
|
|
632
|
+
|
|
633
|
+
@staticmethod
|
|
634
|
+
def get_max_record(records: list[RecordModel], field: FieldIdentifier) -> RecordModel:
|
|
635
|
+
"""
|
|
636
|
+
Get the record model with the maximum value of a given field from a list of record models.
|
|
637
|
+
|
|
638
|
+
:param records: The list of record models to search through.
|
|
639
|
+
:param field: The field to find the maximum value of.
|
|
640
|
+
:return: The record model with the maximum value of the given field.
|
|
641
|
+
"""
|
|
642
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
643
|
+
return max(records, key=lambda x: x.get_field_value(field))
|
|
644
|
+
|
|
645
|
+
@staticmethod
|
|
646
|
+
def get_from_all(records: Iterable[RecordModel], getter: _PropertyGetter[_PropertyType]) -> list[_PropertyType]:
|
|
647
|
+
"""
|
|
648
|
+
Use a getter property on all records in a list of record models. For example, you can iterate over a list of
|
|
649
|
+
record models using a getter of Ancestors.of_type(SampleModel) to get all the SampleModel ancestors from each
|
|
650
|
+
record.
|
|
651
|
+
|
|
652
|
+
:param records: The list of record models to get the property from.
|
|
653
|
+
:param getter: The getter to use to get the property from each record.
|
|
654
|
+
:return: A list of the property values from the input record models. The value at the matching index of the
|
|
655
|
+
input records is the results of using the getter on that record.
|
|
656
|
+
"""
|
|
657
|
+
return [x.get(getter) for x in records]
|
|
658
|
+
|
|
659
|
+
@staticmethod
|
|
660
|
+
def set_on_all(records: Iterable[RecordModel], setter: _PropertySetter[_PropertyType]) -> list[_PropertyType]:
|
|
661
|
+
"""
|
|
662
|
+
Use a setter property on all records in a list of record models. For example, you can iterate over a list of
|
|
663
|
+
record models user a setter of ForwardSideLink.ref(field_name, record) to set a forward side link on each
|
|
664
|
+
record.
|
|
665
|
+
|
|
666
|
+
:param records: The list of record models to set the property on.
|
|
667
|
+
:param setter: The setter to use to set the property on each record.
|
|
668
|
+
:return: A list of the property values that were set on the input record models. The value at the matching index
|
|
669
|
+
of the input records is the results of using the setter on that record.
|
|
670
|
+
"""
|
|
671
|
+
return [x.set(setter) for x in records]
|
|
672
|
+
|
|
673
|
+
@staticmethod
|
|
674
|
+
def add_to_all(records: Iterable[RecordModel], adder: _PropertyAdder[_PropertyType]) -> list[_PropertyType]:
|
|
675
|
+
"""
|
|
676
|
+
Use an adder property on all records in a list of record models. For example, you can iterate over a list of
|
|
677
|
+
record models using an adder of Child.create(SampleModel) to create a new SampleModel child on each record.
|
|
678
|
+
|
|
679
|
+
:param records: The list of record models to add the property to.
|
|
680
|
+
:param adder: The adder to use to add the property to each record.
|
|
681
|
+
:return: A list of the property values that were added to the input record models. The value at the matching
|
|
682
|
+
index of the input records is the results of using the adder on that record.
|
|
683
|
+
"""
|
|
684
|
+
return [x.add(adder) for x in records]
|
|
685
|
+
|
|
686
|
+
@staticmethod
|
|
687
|
+
def remove_from_all(records: Iterable[RecordModel], remover: _PropertyRemover[_PropertyType]) -> list[_PropertyType]:
|
|
688
|
+
"""
|
|
689
|
+
Use a remover property on all records in a list of record models. For example, you can iterate over a list of
|
|
690
|
+
record models using a remover of Parents.ref(records) to remove a list of parents from each record.
|
|
691
|
+
|
|
692
|
+
:param records: The list of record models to remove the property from.
|
|
693
|
+
:param remover: The remover to use to remove the property from each record.
|
|
694
|
+
:return: A list of the property values that were removed from the input record models. The value at the matching
|
|
695
|
+
index of the input records is the results of using the remover on that record.
|
|
696
|
+
"""
|
|
697
|
+
return [x.remove(remover) for x in records]
|
|
698
|
+
|
|
699
|
+
# FR-47527: Created functions for manipulating relationships between records,
|
|
700
|
+
def get_extension(self, model: RecordModel, wrapper_type: type[WrappedType] | str) \
|
|
701
|
+
-> WrappedType | PyRecordModel | None:
|
|
702
|
+
"""
|
|
703
|
+
Given a record with an extension record related to it, return the extension record as a record model.
|
|
704
|
+
This will retrieve an extension record without doing a webservice request to the server. The input record and
|
|
705
|
+
extension record will be considered related to one another if you later use load_child or load_parent on the
|
|
706
|
+
input record or extension record respectively.
|
|
707
|
+
|
|
708
|
+
:param model: The record model to get the extension for.
|
|
709
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the extension record. If a data
|
|
710
|
+
type name is provided, the returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
711
|
+
:return: The extension record model for the input record model, or None if no extension record exists.
|
|
712
|
+
"""
|
|
713
|
+
ext_dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
714
|
+
ext_fields: FieldMap = {}
|
|
715
|
+
for field, value in AliasUtil.to_field_map(model).items():
|
|
716
|
+
if field.startswith(ext_dt + "."):
|
|
717
|
+
ext_fields[field.removeprefix(ext_dt + ".")] = value
|
|
718
|
+
if not ext_fields or ext_fields.get("RecordId") is None:
|
|
719
|
+
return None
|
|
720
|
+
ext_rec: DataRecord = DataRecord(ext_dt, ext_fields.get("RecordId"), ext_fields)
|
|
721
|
+
ext_model: WrappedType | PyRecordModel = self.wrap_model(ext_rec, wrapper_type)
|
|
722
|
+
self._spoof_child_load(model, ext_model)
|
|
723
|
+
self._spoof_parent_load(ext_model, model)
|
|
724
|
+
return ext_model
|
|
725
|
+
|
|
726
|
+
def get_or_add_parent(self, record: RecordModel, parent_type: type[WrappedType] | str) \
|
|
727
|
+
-> WrappedType | PyRecordModel:
|
|
728
|
+
"""
|
|
729
|
+
Given a record model, retrieve the singular parent record model of a given type. If a parent of the given type
|
|
730
|
+
does not exist, a new one will be created. The parents of the given data type must already be loaded.
|
|
731
|
+
|
|
732
|
+
:param record: The record model to get the parent of.
|
|
733
|
+
:param parent_type: The record model wrapper of the parent, or the data type name of the parent. If a data type
|
|
734
|
+
name is provided, the returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
735
|
+
:return: The parent record model of the given type.
|
|
736
|
+
"""
|
|
737
|
+
parent_dt: str = AliasUtil.to_data_type_name(parent_type)
|
|
738
|
+
wrapper: type[WrappedType] | None = parent_type if isinstance(parent_type, type) else None
|
|
739
|
+
record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
|
|
740
|
+
parent: PyRecordModel | None = record.get_parent_of_type(parent_dt)
|
|
741
|
+
if parent is not None:
|
|
742
|
+
return self.wrap_model(record, wrapper) if wrapper else parent
|
|
743
|
+
return record.add(Parent.create(wrapper)) if wrapper else record.add(Parent.create_by_name(parent_dt))
|
|
744
|
+
|
|
745
|
+
def get_or_add_child(self, record: RecordModel, child_type: type[WrappedType] | str) -> WrappedType | PyRecordModel:
|
|
746
|
+
"""
|
|
747
|
+
Given a record model, retrieve the singular child record model of a given type. If a child of the given type
|
|
748
|
+
does not exist, a new one will be created. The children of the given data type must already be loaded.
|
|
749
|
+
|
|
750
|
+
:param record: The record model to get the child of.
|
|
751
|
+
:param child_type: The record model wrapper of the child, or the data type name of the child. If a data type
|
|
752
|
+
name is provided, the returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
753
|
+
:return: The child record model of the given type.
|
|
754
|
+
"""
|
|
755
|
+
child_dt: str = AliasUtil.to_data_type_name(child_type)
|
|
756
|
+
wrapper: type[WrappedType] | None = child_type if isinstance(child_type, type) else None
|
|
757
|
+
record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
|
|
758
|
+
child: PyRecordModel | None = record.get_child_of_type(child_dt)
|
|
759
|
+
if child is not None:
|
|
760
|
+
return self.wrap_model(record, wrapper) if wrapper else child
|
|
761
|
+
return record.add(Child.create(wrapper)) if wrapper else record.add(Child.create_by_name(child_dt))
|
|
762
|
+
|
|
763
|
+
def get_or_add_side_link(self, record: RecordModel, side_link_field: FieldIdentifier,
|
|
764
|
+
side_link_type: type[WrappedType] | str) -> WrappedType | PyRecordModel:
|
|
765
|
+
"""
|
|
766
|
+
Given a record model, retrieve the singular side link record model of a given type. If a side link of the given
|
|
767
|
+
type does not exist, a new one will be created. The side links of the given data type must already be loaded.
|
|
768
|
+
|
|
769
|
+
:param record: The record model to get the side link of.
|
|
770
|
+
:param side_link_field: The field name of the side link to get.
|
|
771
|
+
:param side_link_type: The record model wrapper of the side link, or the data type name of the side link. If a
|
|
772
|
+
data type name is provided, the returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
773
|
+
:return: The side link record model of the given type.
|
|
774
|
+
"""
|
|
775
|
+
side_link_field: str = AliasUtil.to_data_field_name(side_link_field)
|
|
776
|
+
wrapper: type[WrappedType] | None = side_link_type if isinstance(side_link_type, type) else None
|
|
777
|
+
record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
|
|
778
|
+
side_link: PyRecordModel | None = record.get_forward_side_link(side_link_field)
|
|
779
|
+
if side_link is not None:
|
|
780
|
+
return self.wrap_model(record, wrapper) if wrapper else side_link
|
|
781
|
+
side_link: WrappedType | PyRecordModel = self.add_model(side_link_type)
|
|
782
|
+
return record.add(ForwardSideLink.ref(side_link_field, side_link))
|
|
783
|
+
|
|
784
|
+
@staticmethod
|
|
785
|
+
def set_parents(record: RecordModel, parents: Iterable[RecordModel], parent_type: DataTypeIdentifier) -> None:
|
|
786
|
+
"""
|
|
787
|
+
Set the parents of a record model to a list of parent record models of a given type. The parents of the given
|
|
788
|
+
data type must already be loaded. This method will add the parents to the record model if they are not already
|
|
789
|
+
parents, and remove any existing parents that are not in the input list.
|
|
790
|
+
|
|
791
|
+
:param record: The record model to set the parents of.
|
|
792
|
+
:param parents: The list of parent record models to set as the parents of the input record model.
|
|
793
|
+
:param parent_type: The data type identifier of the parent record models.
|
|
794
|
+
"""
|
|
795
|
+
parent_dt: str = AliasUtil.to_data_type_name(parent_type)
|
|
796
|
+
existing_parents: list[PyRecordModel] = record.get(Parents.of_type_name(parent_dt))
|
|
797
|
+
for parent in parents:
|
|
798
|
+
if parent not in existing_parents:
|
|
799
|
+
record.add(Parent.ref(parent))
|
|
800
|
+
for parent in existing_parents:
|
|
801
|
+
if parent not in parents:
|
|
802
|
+
record.remove(Parent.ref(parent))
|
|
803
|
+
|
|
804
|
+
@staticmethod
|
|
805
|
+
def set_children(record: RecordModel, children: Iterable[RecordModel], child_type: DataTypeIdentifier) -> None:
|
|
806
|
+
"""
|
|
807
|
+
Set the children of a record model to a list of child record models of a given type. The children of the given
|
|
808
|
+
data type must already be loaded. This method will add the children to the record model if they are not already
|
|
809
|
+
children, and remove any existing children that are not in the input list.
|
|
810
|
+
|
|
811
|
+
:param record: The record model to set the children of.
|
|
812
|
+
:param children: The list of child record models to set as the children of the input record model.
|
|
813
|
+
:param child_type: The data type identifier of the child record models.
|
|
814
|
+
"""
|
|
815
|
+
child_dt: str = AliasUtil.to_data_type_name(child_type)
|
|
816
|
+
existing_children: list[PyRecordModel] = record.get(Children.of_type_name(child_dt))
|
|
817
|
+
for child in children:
|
|
818
|
+
if child not in existing_children:
|
|
819
|
+
record.add(Child.ref(child))
|
|
820
|
+
for child in existing_children:
|
|
821
|
+
if child not in children:
|
|
822
|
+
record.remove(Child.ref(child))
|
|
823
|
+
|
|
475
824
|
@staticmethod
|
|
476
|
-
def map_to_parent(models: Iterable[
|
|
825
|
+
def map_to_parent(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType])\
|
|
826
|
+
-> dict[WrappedRecordModel, WrappedType]:
|
|
477
827
|
"""
|
|
478
828
|
Map a list of record models to a single parent of a given type. The parents must already be loaded.
|
|
479
829
|
|
|
@@ -482,14 +832,14 @@ class RecordHandler:
|
|
|
482
832
|
:return: A dict[ModelType, ParentType]. If an input model doesn't have a parent of the given parent type, then
|
|
483
833
|
it will map to None.
|
|
484
834
|
"""
|
|
485
|
-
return_dict: dict[
|
|
835
|
+
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
486
836
|
for model in models:
|
|
487
837
|
return_dict[model] = model.get_parent_of_type(parent_type)
|
|
488
838
|
return return_dict
|
|
489
839
|
|
|
490
840
|
@staticmethod
|
|
491
|
-
def map_to_parents(models: Iterable[
|
|
492
|
-
-> dict[
|
|
841
|
+
def map_to_parents(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
|
|
842
|
+
-> dict[WrappedRecordModel, list[WrappedType]]:
|
|
493
843
|
"""
|
|
494
844
|
Map a list of record models to a list parents of a given type. The parents must already be loaded.
|
|
495
845
|
|
|
@@ -498,14 +848,14 @@ class RecordHandler:
|
|
|
498
848
|
:return: A dict[ModelType, list[ParentType]]. If an input model doesn't have a parent of the given parent type,
|
|
499
849
|
then it will map to an empty list.
|
|
500
850
|
"""
|
|
501
|
-
return_dict: dict[
|
|
851
|
+
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
502
852
|
for model in models:
|
|
503
853
|
return_dict[model] = model.get_parents_of_type(parent_type)
|
|
504
854
|
return return_dict
|
|
505
855
|
|
|
506
856
|
@staticmethod
|
|
507
|
-
def map_by_parent(models: Iterable[
|
|
508
|
-
-> dict[WrappedType,
|
|
857
|
+
def map_by_parent(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
|
|
858
|
+
-> dict[WrappedType, WrappedRecordModel]:
|
|
509
859
|
"""
|
|
510
860
|
Take a list of record models and map them by their parent. Essentially an inversion of map_to_parent.
|
|
511
861
|
If two records share the same parent, an exception will be thrown. The parents must already be loaded.
|
|
@@ -515,8 +865,8 @@ class RecordHandler:
|
|
|
515
865
|
:return: A dict[ParentType, ModelType]. If an input model doesn't have a parent of the given parent type,
|
|
516
866
|
then it will not be in the resulting dictionary.
|
|
517
867
|
"""
|
|
518
|
-
to_parent: dict[
|
|
519
|
-
by_parent: dict[WrappedType,
|
|
868
|
+
to_parent: dict[WrappedRecordModel, WrappedType] = RecordHandler.map_to_parent(models, parent_type)
|
|
869
|
+
by_parent: dict[WrappedType, WrappedRecordModel] = {}
|
|
520
870
|
for record, parent in to_parent.items():
|
|
521
871
|
if parent is None:
|
|
522
872
|
continue
|
|
@@ -527,8 +877,8 @@ class RecordHandler:
|
|
|
527
877
|
return by_parent
|
|
528
878
|
|
|
529
879
|
@staticmethod
|
|
530
|
-
def map_by_parents(models: Iterable[
|
|
531
|
-
-> dict[WrappedType, list[
|
|
880
|
+
def map_by_parents(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
|
|
881
|
+
-> dict[WrappedType, list[WrappedRecordModel]]:
|
|
532
882
|
"""
|
|
533
883
|
Take a list of record models and map them by their parents. Essentially an inversion of map_to_parents. Input
|
|
534
884
|
models that share a parent will end up in the same list. The parents must already be loaded.
|
|
@@ -538,15 +888,16 @@ class RecordHandler:
|
|
|
538
888
|
:return: A dict[ParentType, list[ModelType]]. If an input model doesn't have a parent of the given parent type,
|
|
539
889
|
then it will not be in the resulting dictionary.
|
|
540
890
|
"""
|
|
541
|
-
to_parents: dict[
|
|
542
|
-
by_parents: dict[WrappedType, list[
|
|
891
|
+
to_parents: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler.map_to_parents(models, parent_type)
|
|
892
|
+
by_parents: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
543
893
|
for record, parents in to_parents.items():
|
|
544
894
|
for parent in parents:
|
|
545
895
|
by_parents.setdefault(parent, []).append(record)
|
|
546
896
|
return by_parents
|
|
547
897
|
|
|
548
898
|
@staticmethod
|
|
549
|
-
def map_to_child(models: Iterable[
|
|
899
|
+
def map_to_child(models: Iterable[WrappedRecordModel], child_type: type[WrappedType])\
|
|
900
|
+
-> dict[WrappedRecordModel, WrappedType]:
|
|
550
901
|
"""
|
|
551
902
|
Map a list of record models to a single child of a given type. The children must already be loaded.
|
|
552
903
|
|
|
@@ -555,14 +906,14 @@ class RecordHandler:
|
|
|
555
906
|
:return: A dict[ModelType, ChildType]. If an input model doesn't have a child of the given child type, then
|
|
556
907
|
it will map to None.
|
|
557
908
|
"""
|
|
558
|
-
return_dict: dict[
|
|
909
|
+
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
559
910
|
for model in models:
|
|
560
911
|
return_dict[model] = model.get_child_of_type(child_type)
|
|
561
912
|
return return_dict
|
|
562
913
|
|
|
563
914
|
@staticmethod
|
|
564
|
-
def map_to_children(models: Iterable[
|
|
565
|
-
-> dict[
|
|
915
|
+
def map_to_children(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
|
|
916
|
+
-> dict[WrappedRecordModel, list[WrappedType]]:
|
|
566
917
|
"""
|
|
567
918
|
Map a list of record models to a list children of a given type. The children must already be loaded.
|
|
568
919
|
|
|
@@ -571,14 +922,14 @@ class RecordHandler:
|
|
|
571
922
|
:return: A dict[ModelType, list[ChildType]]. If an input model doesn't have children of the given child type,
|
|
572
923
|
then it will map to an empty list.
|
|
573
924
|
"""
|
|
574
|
-
return_dict: dict[
|
|
925
|
+
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
575
926
|
for model in models:
|
|
576
927
|
return_dict[model] = model.get_children_of_type(child_type)
|
|
577
928
|
return return_dict
|
|
578
929
|
|
|
579
930
|
@staticmethod
|
|
580
|
-
def map_by_child(models: Iterable[
|
|
581
|
-
-> dict[WrappedType,
|
|
931
|
+
def map_by_child(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
|
|
932
|
+
-> dict[WrappedType, WrappedRecordModel]:
|
|
582
933
|
"""
|
|
583
934
|
Take a list of record models and map them by their children. Essentially an inversion of map_to_child.
|
|
584
935
|
If two records share the same child, an exception will be thrown. The children must already be loaded.
|
|
@@ -588,8 +939,8 @@ class RecordHandler:
|
|
|
588
939
|
:return: A dict[ChildType, ModelType]. If an input model doesn't have a child of the given child type,
|
|
589
940
|
then it will not be in the resulting dictionary.
|
|
590
941
|
"""
|
|
591
|
-
to_child: dict[
|
|
592
|
-
by_child: dict[WrappedType,
|
|
942
|
+
to_child: dict[WrappedRecordModel, WrappedType] = RecordHandler.map_to_child(models, child_type)
|
|
943
|
+
by_child: dict[WrappedType, WrappedRecordModel] = {}
|
|
593
944
|
for record, child in to_child.items():
|
|
594
945
|
if child is None:
|
|
595
946
|
continue
|
|
@@ -600,8 +951,8 @@ class RecordHandler:
|
|
|
600
951
|
return by_child
|
|
601
952
|
|
|
602
953
|
@staticmethod
|
|
603
|
-
def map_by_children(models: Iterable[
|
|
604
|
-
-> dict[WrappedType, list[
|
|
954
|
+
def map_by_children(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
|
|
955
|
+
-> dict[WrappedType, list[WrappedRecordModel]]:
|
|
605
956
|
"""
|
|
606
957
|
Take a list of record models and map them by their children. Essentially an inversion of map_to_children. Input
|
|
607
958
|
models that share a child will end up in the same list. The children must already be loaded.
|
|
@@ -611,8 +962,8 @@ class RecordHandler:
|
|
|
611
962
|
:return: A dict[ChildType, list[ModelType]]. If an input model doesn't have children of the given child type,
|
|
612
963
|
then it will not be in the resulting dictionary.
|
|
613
964
|
"""
|
|
614
|
-
to_children: dict[
|
|
615
|
-
by_children: dict[WrappedType, list[
|
|
965
|
+
to_children: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler.map_to_children(models, child_type)
|
|
966
|
+
by_children: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
616
967
|
for record, children in to_children.items():
|
|
617
968
|
for child in children:
|
|
618
969
|
by_children.setdefault(child, []).append(record)
|
|
@@ -851,7 +1202,7 @@ class RecordHandler:
|
|
|
851
1202
|
return field_sum
|
|
852
1203
|
|
|
853
1204
|
@staticmethod
|
|
854
|
-
def mean_of_field(models:
|
|
1205
|
+
def mean_of_field(models: Collection[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
855
1206
|
"""
|
|
856
1207
|
Calculate the mean of the numeric value of a given field across all input models. Excepts that all given models
|
|
857
1208
|
have a value. If the field is an integer field, the value will be converted to a float.
|
|
@@ -860,7 +1211,7 @@ class RecordHandler:
|
|
|
860
1211
|
:param field_name: The name of the numeric field to mean.
|
|
861
1212
|
:return: The mean of the field values for the collection of models.
|
|
862
1213
|
"""
|
|
863
|
-
return RecordHandler.sum_of_field(models, field_name) / len(
|
|
1214
|
+
return RecordHandler.sum_of_field(models, field_name) / len(models)
|
|
864
1215
|
|
|
865
1216
|
@staticmethod
|
|
866
1217
|
def get_newest_record(records: Iterable[SapioRecord]) -> SapioRecord:
|
|
@@ -870,11 +1221,7 @@ class RecordHandler:
|
|
|
870
1221
|
:param records: The list of records.
|
|
871
1222
|
:return: The input record with the highest record ID. None if the input list is empty.
|
|
872
1223
|
"""
|
|
873
|
-
|
|
874
|
-
for record in records:
|
|
875
|
-
if newest is None or record.record_id > newest.record_id:
|
|
876
|
-
newest = record
|
|
877
|
-
return newest
|
|
1224
|
+
return max(records, key=lambda x: x.record_id)
|
|
878
1225
|
|
|
879
1226
|
# FR-46696: Add a function for getting the oldest record in a list, just like we have one for the newest record.
|
|
880
1227
|
@staticmethod
|
|
@@ -885,22 +1232,18 @@ class RecordHandler:
|
|
|
885
1232
|
:param records: The list of records.
|
|
886
1233
|
:return: The input record with the lowest record ID. None if the input list is empty.
|
|
887
1234
|
"""
|
|
888
|
-
|
|
889
|
-
for record in records:
|
|
890
|
-
if oldest is None or record.record_id < oldest.record_id:
|
|
891
|
-
oldest = record
|
|
892
|
-
return oldest
|
|
1235
|
+
return min(records, key=lambda x: x.record_id)
|
|
893
1236
|
|
|
894
1237
|
@staticmethod
|
|
895
1238
|
def values_to_field_maps(field_name: FieldIdentifier, values: Iterable[FieldValue],
|
|
896
|
-
existing_fields: list[
|
|
1239
|
+
existing_fields: list[FieldMap] | None = None) -> list[FieldMap]:
|
|
897
1240
|
"""
|
|
898
1241
|
Add a list of values for a specific field to a list of dictionaries pairing each value to that field name.
|
|
899
1242
|
|
|
900
1243
|
:param field_name: The name of the field that the values are from.
|
|
901
1244
|
:param values: A list of field values.
|
|
902
1245
|
:param existing_fields: An optional existing fields map list to add the new values to. Values are added in the
|
|
903
|
-
|
|
1246
|
+
list in the same order that they appear. If no existing fields are provided, returns a new fields map list.
|
|
904
1247
|
:return: A fields map list that contains the given values mapped by the given field name.
|
|
905
1248
|
"""
|
|
906
1249
|
# Update the existing fields map list if one is given.
|
|
@@ -919,8 +1262,9 @@ class RecordHandler:
|
|
|
919
1262
|
|
|
920
1263
|
# FR-46155: Update relationship path traversing functions to be non-static and take in a wrapper type so that the
|
|
921
1264
|
# output can be wrapped instead of requiring the user to wrap the output.
|
|
922
|
-
def get_linear_path(self, models: Iterable[RecordModel], path: RelationshipPath,
|
|
923
|
-
|
|
1265
|
+
def get_linear_path(self, models: Iterable[RecordModel], path: RelationshipPath,
|
|
1266
|
+
wrapper_type: type[WrappedType] | None = None) \
|
|
1267
|
+
-> dict[RecordModel, WrappedType | PyRecordModel | None]:
|
|
924
1268
|
"""
|
|
925
1269
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
926
1270
|
path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
|
|
@@ -928,7 +1272,8 @@ class RecordHandler:
|
|
|
928
1272
|
|
|
929
1273
|
:param models: A list of record models.
|
|
930
1274
|
:param path: The relationship path to follow.
|
|
931
|
-
:param wrapper_type: The record model wrapper to use.
|
|
1275
|
+
:param wrapper_type: The record model wrapper to use on the record at the end of the path. If not provided,
|
|
1276
|
+
the record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
932
1277
|
:return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
|
|
933
1278
|
path couldn't be reached, the record will map to None.
|
|
934
1279
|
"""
|
|
@@ -976,11 +1321,12 @@ class RecordHandler:
|
|
|
976
1321
|
current = reverse_links[0]
|
|
977
1322
|
else:
|
|
978
1323
|
raise SapioException("Unsupported path direction.")
|
|
979
|
-
ret_dict.update({model: self.
|
|
1324
|
+
ret_dict.update({model: self.wrap_model(current, wrapper_type) if current else None})
|
|
980
1325
|
return ret_dict
|
|
981
1326
|
|
|
982
1327
|
def get_branching_path(self, models: Iterable[RecordModel], path: RelationshipPath,
|
|
983
|
-
wrapper_type: type[WrappedType]
|
|
1328
|
+
wrapper_type: type[WrappedType] | None = None)\
|
|
1329
|
+
-> dict[RecordModel, list[WrappedType] | list[PyRecordModel]]:
|
|
984
1330
|
"""
|
|
985
1331
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
986
1332
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
@@ -988,7 +1334,8 @@ class RecordHandler:
|
|
|
988
1334
|
|
|
989
1335
|
:param models: A list of record models.
|
|
990
1336
|
:param path: The relationship path to follow.
|
|
991
|
-
:param wrapper_type: The record model wrapper to use.
|
|
1337
|
+
:param wrapper_type: The record model wrapper to use on the records at the end of the path. If not provided,
|
|
1338
|
+
the records will be PyRecordModels instead of WrappedRecordModels.
|
|
992
1339
|
:return: Each record model mapped to the records at the end of the path starting from itself. If the end of the
|
|
993
1340
|
path couldn't be reached, the record will map to an empty list.
|
|
994
1341
|
"""
|
|
@@ -1021,13 +1368,14 @@ class RecordHandler:
|
|
|
1021
1368
|
raise SapioException("Unsupported path direction.")
|
|
1022
1369
|
current_search = next_search
|
|
1023
1370
|
next_search = set()
|
|
1024
|
-
ret_dict.update({model: self.
|
|
1371
|
+
ret_dict.update({model: self.wrap_models(current_search, wrapper_type)})
|
|
1025
1372
|
return ret_dict
|
|
1026
1373
|
|
|
1027
1374
|
# FR-46155: Create a relationship traversing function that returns a single function at the end of the path like
|
|
1028
1375
|
# get_linear_path but can handle branching paths in the middle of the search like get_branching_path.
|
|
1029
|
-
def get_flat_path(self, models: Iterable[RecordModel], path: RelationshipPath,
|
|
1030
|
-
|
|
1376
|
+
def get_flat_path(self, models: Iterable[RecordModel], path: RelationshipPath,
|
|
1377
|
+
wrapper_type: type[WrappedType] | None = None) \
|
|
1378
|
+
-> dict[RecordModel, WrappedType | PyRecordModel | None]:
|
|
1031
1379
|
"""
|
|
1032
1380
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
1033
1381
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
@@ -1039,7 +1387,8 @@ class RecordHandler:
|
|
|
1039
1387
|
|
|
1040
1388
|
:param models: A list of record models.
|
|
1041
1389
|
:param path: The relationship path to follow.
|
|
1042
|
-
:param wrapper_type: The record model wrapper to use.
|
|
1390
|
+
:param wrapper_type: The record model wrapper to use on the record at the end of the path. If not provided,
|
|
1391
|
+
the record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
1043
1392
|
:return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
|
|
1044
1393
|
path couldn't be reached, the record will map to None.
|
|
1045
1394
|
"""
|
|
@@ -1067,21 +1416,22 @@ class RecordHandler:
|
|
|
1067
1416
|
current = current[0].get_reverse_side_link(data_type, node.data_field_name)
|
|
1068
1417
|
else:
|
|
1069
1418
|
raise SapioException("Unsupported path direction.")
|
|
1070
|
-
ret_dict.update({model: self.
|
|
1419
|
+
ret_dict.update({model: self.wrap_model(current[0], wrapper_type) if current else None})
|
|
1071
1420
|
return ret_dict
|
|
1072
1421
|
|
|
1073
|
-
def __find_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value: FieldValue,
|
|
1074
|
-
secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType | None:
|
|
1422
|
+
def __find_model(self, wrapper_type: type[WrappedType] | str, primary_identifier: str, id_value: FieldValue,
|
|
1423
|
+
secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType | PyRecordModel | None:
|
|
1075
1424
|
"""
|
|
1076
1425
|
Find a record from the system that matches the given field values. The primary identifier and value is used
|
|
1077
1426
|
to query for the record, then the secondary identifiers may be optionally provided to further filter the
|
|
1078
1427
|
returned results. If no record is found with these filters, returns None.
|
|
1079
1428
|
"""
|
|
1080
1429
|
# Query for all records that match the primary identifier.
|
|
1081
|
-
results: list[WrappedType] = self.query_models(wrapper_type, primary_identifier,
|
|
1430
|
+
results: list[WrappedType] | list[PyRecordModel] = self.query_models(wrapper_type, primary_identifier,
|
|
1431
|
+
[id_value])
|
|
1082
1432
|
|
|
1083
1433
|
# Find the one record, if any, that matches the secondary identifiers.
|
|
1084
|
-
unique_record: WrappedType | None = None
|
|
1434
|
+
unique_record: WrappedType | PyRecordModel | None = None
|
|
1085
1435
|
for result in results:
|
|
1086
1436
|
matches_all: bool = True
|
|
1087
1437
|
for field, value in secondary_identifiers.items():
|
|
@@ -1091,22 +1441,60 @@ class RecordHandler:
|
|
|
1091
1441
|
if matches_all:
|
|
1092
1442
|
# If a previous record in the results already matched all identifiers, then throw an exception.
|
|
1093
1443
|
if unique_record is not None:
|
|
1094
|
-
raise SapioException(f"More than one record of type {
|
|
1444
|
+
raise SapioException(f"More than one record of type {AliasUtil.to_data_type_name(wrapper_type)} "
|
|
1095
1445
|
f"encountered in system that matches all provided identifiers.")
|
|
1096
1446
|
unique_record = result
|
|
1097
1447
|
return unique_record
|
|
1098
1448
|
|
|
1099
1449
|
@staticmethod
|
|
1100
|
-
def __verify_data_type(
|
|
1450
|
+
def __verify_data_type(record: DataRecord | PyRecordModel, wrapper_type: type[WrappedType]) -> None:
|
|
1101
1451
|
"""
|
|
1102
|
-
Throw an exception if the data type of the given
|
|
1452
|
+
Throw an exception if the data type of the given record and wrapper don't match.
|
|
1103
1453
|
"""
|
|
1104
1454
|
model_type: str = wrapper_type.get_wrapper_data_type_name()
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1455
|
+
record_type: str = AliasUtil.to_data_type_name(record)
|
|
1456
|
+
# Account for ELN data type records.
|
|
1457
|
+
if ElnBaseDataType.is_eln_type(record_type):
|
|
1458
|
+
record_type = ElnBaseDataType.get_base_type(record_type).data_type_name
|
|
1459
|
+
if record_type != model_type:
|
|
1460
|
+
raise SapioException(f"Data record of type {record_type} cannot be wrapped by the record model wrapper "
|
|
1461
|
+
f"of type {model_type}")
|
|
1462
|
+
|
|
1463
|
+
@staticmethod
|
|
1464
|
+
def _spoof_child_load(model: RecordModel, child: RecordModel) -> None:
|
|
1465
|
+
"""
|
|
1466
|
+
Spoof the loading of a child record on a record model. This is useful for when you have records that you know
|
|
1467
|
+
are related but didn't use the relationship manager to load the relationship, which would make a webservice
|
|
1468
|
+
call.
|
|
1469
|
+
"""
|
|
1470
|
+
RecordHandler._spoof_children_load(model, [child])
|
|
1471
|
+
|
|
1472
|
+
@staticmethod
|
|
1473
|
+
def _spoof_children_load(model: RecordModel, children: list[RecordModel]) -> None:
|
|
1474
|
+
"""
|
|
1475
|
+
Spoof the loading of child records on a record model. This is useful for when you have records that you know
|
|
1476
|
+
are related but didn't use the relationship manager to load the relationship, which would make a webservice
|
|
1477
|
+
"""
|
|
1478
|
+
model: PyRecordModel = RecordModelInstanceManager.unwrap(model)
|
|
1479
|
+
child_dt: str = AliasUtil.to_singular_data_type_name(children)
|
|
1480
|
+
# noinspection PyProtectedMember
|
|
1481
|
+
model._mark_children_loaded(child_dt, RecordModelInstanceManager.unwrap_list(children))
|
|
1482
|
+
|
|
1483
|
+
@staticmethod
|
|
1484
|
+
def _spoof_parent_load(model: RecordModel, parent: RecordModel) -> None:
|
|
1485
|
+
"""
|
|
1486
|
+
Spoof the loading of a parent record on a record model. This is useful for when you have records that you know
|
|
1487
|
+
are related but didn't use the relationship manager to load the relationship, which would make a webservice
|
|
1488
|
+
"""
|
|
1489
|
+
RecordHandler._spoof_parents_load(model, [parent])
|
|
1490
|
+
|
|
1491
|
+
@staticmethod
|
|
1492
|
+
def _spoof_parents_load(model: RecordModel, parents: list[RecordModel]) -> None:
|
|
1493
|
+
"""
|
|
1494
|
+
Spoof the loading of parent records on a record model. This is useful for when you have records that you know
|
|
1495
|
+
are related but didn't use the relationship manager to load the relationship, which would make a webservice
|
|
1496
|
+
"""
|
|
1497
|
+
model: PyRecordModel = RecordModelInstanceManager.unwrap(model)
|
|
1498
|
+
parent_dt: str = AliasUtil.to_singular_data_type_name(parents)
|
|
1499
|
+
# noinspection PyProtectedMember
|
|
1500
|
+
model._mark_children_loaded(parent_dt, RecordModelInstanceManager.unwrap_list(parents))
|