sapiopycommons 2025.4.8a474__py3-none-any.whl → 2025.4.9a150__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.

Files changed (42) hide show
  1. sapiopycommons/callbacks/callback_util.py +392 -1262
  2. sapiopycommons/callbacks/field_builder.py +0 -2
  3. sapiopycommons/chem/Molecules.py +2 -0
  4. sapiopycommons/customreport/term_builder.py +1 -1
  5. sapiopycommons/datatype/attachment_util.py +2 -4
  6. sapiopycommons/datatype/data_fields.py +1 -23
  7. sapiopycommons/eln/experiment_handler.py +279 -933
  8. sapiopycommons/eln/experiment_report_util.py +10 -15
  9. sapiopycommons/eln/plate_designer.py +59 -159
  10. sapiopycommons/files/file_bridge.py +0 -76
  11. sapiopycommons/files/file_bridge_handler.py +110 -325
  12. sapiopycommons/files/file_data_handler.py +2 -2
  13. sapiopycommons/files/file_util.py +15 -40
  14. sapiopycommons/files/file_validator.py +5 -6
  15. sapiopycommons/files/file_writer.py +1 -1
  16. sapiopycommons/flowcyto/flow_cyto.py +1 -1
  17. sapiopycommons/general/accession_service.py +3 -3
  18. sapiopycommons/general/aliases.py +28 -51
  19. sapiopycommons/general/audit_log.py +2 -2
  20. sapiopycommons/general/custom_report_util.py +1 -24
  21. sapiopycommons/general/exceptions.py +2 -41
  22. sapiopycommons/general/popup_util.py +2 -2
  23. sapiopycommons/multimodal/multimodal.py +0 -1
  24. sapiopycommons/processtracking/custom_workflow_handler.py +30 -46
  25. sapiopycommons/recordmodel/record_handler.py +159 -547
  26. sapiopycommons/rules/eln_rule_handler.py +30 -41
  27. sapiopycommons/rules/on_save_rule_handler.py +30 -41
  28. sapiopycommons/webhook/webhook_handlers.py +55 -448
  29. sapiopycommons/webhook/webservice_handlers.py +2 -2
  30. {sapiopycommons-2025.4.8a474.dist-info → sapiopycommons-2025.4.9a150.dist-info}/METADATA +1 -1
  31. sapiopycommons-2025.4.9a150.dist-info/RECORD +59 -0
  32. sapiopycommons/customreport/auto_pagers.py +0 -281
  33. sapiopycommons/eln/experiment_cache.py +0 -173
  34. sapiopycommons/eln/experiment_step_factory.py +0 -474
  35. sapiopycommons/eln/experiment_tags.py +0 -7
  36. sapiopycommons/eln/step_creation.py +0 -235
  37. sapiopycommons/general/data_structure_util.py +0 -115
  38. sapiopycommons/general/directive_util.py +0 -86
  39. sapiopycommons/samples/aliquot.py +0 -48
  40. sapiopycommons-2025.4.8a474.dist-info/RECORD +0 -67
  41. {sapiopycommons-2025.4.8a474.dist-info → sapiopycommons-2025.4.9a150.dist-info}/WHEEL +0 -0
  42. {sapiopycommons-2025.4.8a474.dist-info → sapiopycommons-2025.4.9a150.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- import io
4
- import warnings
5
3
  from collections.abc import Iterable
6
- from typing import Collection
7
4
  from weakref import WeakValueDictionary
8
5
 
9
6
  from sapiopylib.rest.DataRecordManagerService import DataRecordManager
@@ -15,28 +12,19 @@ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
15
12
  from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
16
13
  from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
17
14
  QueryAllRecordsOfTypeAutoPager
18
- from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel, AbstractRecordModelPropertyGetter, \
19
- RecordModelPropertyType, AbstractRecordModelPropertyAdder, AbstractRecordModelPropertySetter, \
20
- AbstractRecordModelPropertyRemover
15
+ from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
21
16
  from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager, \
22
17
  RecordModelRelationshipManager
23
18
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, WrappedRecordModel
24
19
  from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
25
20
  RelationshipNodeType
26
21
  from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
27
- from sapiopylib.rest.utils.recordmodel.properties import Parents, Parent, Children, Child, ForwardSideLink
28
22
 
29
23
  from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
30
- FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey, DataTypeIdentifier
24
+ FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey
31
25
  from sapiopycommons.general.custom_report_util import CustomReportUtil
32
26
  from sapiopycommons.general.exceptions import SapioException
33
27
 
34
- # Aliases for longer name.
35
- _PropertyGetter = AbstractRecordModelPropertyGetter
36
- _PropertyAdder = AbstractRecordModelPropertyAdder
37
- _PropertyRemover = AbstractRecordModelPropertyRemover
38
- _PropertySetter = AbstractRecordModelPropertySetter
39
- _PropertyType = RecordModelPropertyType
40
28
 
41
29
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
42
30
  class RecordHandler:
@@ -69,273 +57,226 @@ class RecordHandler:
69
57
  """
70
58
  :param context: The current webhook context or a user object to send requests from.
71
59
  """
60
+ self.user = AliasUtil.to_sapio_user(context)
72
61
  if self.__initialized:
73
62
  return
74
63
  self.__initialized = True
75
64
 
76
- self.user = AliasUtil.to_sapio_user(context)
65
+ self.user = context if isinstance(context, SapioUser) else context.user
77
66
  self.dr_man = DataRecordManager(self.user)
78
67
  self.rec_man = RecordModelManager(self.user)
79
68
  self.inst_man = self.rec_man.instance_manager
80
69
  self.rel_man = self.rec_man.relationship_manager
81
70
  self.an_man = RecordModelAncestorManager(self.rec_man)
82
71
 
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:
72
+ def wrap_model(self, record: DataRecord, wrapper_type: type[WrappedType]) -> WrappedType:
86
73
  """
87
- Shorthand for adding a single data record or PyRecordModel as a WrappedRecordModel.
74
+ Shorthand for adding a single data record as a record model.
88
75
 
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.
76
+ :param record: The data record to wrap.
77
+ :param wrapper_type: The record model wrapper to use.
92
78
  :return: The record model for the input.
93
79
  """
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)
80
+ self.__verify_data_type([record], wrapper_type)
81
+ return self.inst_man.add_existing_record_of_type(record, wrapper_type)
102
82
 
103
- def wrap_models(self, records: Iterable[DataRecord | PyRecordModel],
104
- wrapper_type: type[WrappedType] | None = None) \
105
- -> list[WrappedType] | list[PyRecordModel]:
83
+ def wrap_models(self, records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> list[WrappedType]:
106
84
  """
107
- Shorthand for adding a list of data records or PyRecordModels as a WrappedRecordModels.
85
+ Shorthand for adding a list of data records as record models.
108
86
 
109
87
  :param records: The data records to wrap.
110
- :param wrapper_type: The record model wrapper to use. If not provided, the records are returned as
111
- PyRecordModels instead of WrappedRecordModels.
88
+ :param wrapper_type: The record model wrapper to use.
112
89
  :return: The record models for the input.
113
90
  """
114
- return [self.wrap_model(x, wrapper_type) for x in records]
91
+ self.__verify_data_type(records, wrapper_type)
92
+ return self.inst_man.add_existing_records_of_type(list(records), wrapper_type)
115
93
 
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]:
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]:
122
96
  """
123
97
  Shorthand for using the data record manager to query for a list of data records by field value
124
98
  and then converting the results into a list of record models.
125
99
 
126
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
100
+ :param wrapper_type: The record model wrapper to use.
127
101
  :param field: 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.
102
+ :param value_list: The values of the field to query on.
131
103
  :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
132
104
  only functions if you set a page size or the platform enforces a page size.
133
105
  :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
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.
106
+ :return: The record models for the queried records.
136
107
  """
137
108
  criteria: DataRecordPojoPageCriteria | None = None
138
109
  if page_size is not None:
139
110
  criteria = DataRecordPojoPageCriteria(page_size=page_size)
140
111
  return self.query_models_with_criteria(wrapper_type, field, value_list, criteria, page_limit)[0]
141
112
 
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]]:
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]]:
148
117
  """
149
118
  Shorthand for using query_models to search for records given values on a specific field and then using
150
119
  map_by_field to turn the returned list into a dictionary mapping field values to records.
151
120
 
152
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
121
+ :param wrapper_type: The record model wrapper to use.
153
122
  :param field: The field to query and map 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.
123
+ :param value_list: The values of the field to query on.
157
124
  :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
158
125
  only functions if you set a page size or the platform enforces a page size.
159
126
  :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
160
127
  :param mapping_field: If provided, use this field to map against instead of the field that was queried on.
161
128
  :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.
164
129
  """
165
130
  if mapping_field is None:
166
131
  mapping_field = field
167
132
  return self.map_by_field(self.query_models(wrapper_type, field, value_list, page_limit, page_size),
168
133
  mapping_field)
169
134
 
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]:
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]:
176
139
  """
177
140
  Shorthand for using query_models to search for records given values on a specific field and then using
178
141
  map_by_unique_field to turn the returned list into a dictionary mapping field values to records.
179
142
  If any two records share the same field value, throws an exception.
180
143
 
181
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
144
+ :param wrapper_type: The record model wrapper to use.
182
145
  :param field: The field to query and map 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.
146
+ :param value_list: The values of the field to query on.
186
147
  :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
187
148
  only functions if you set a page size or the platform enforces a page size.
188
149
  :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
189
150
  :param mapping_field: If provided, use this field to map against instead of the field that was queried on.
190
151
  :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.
193
152
  """
194
153
  if mapping_field is None:
195
154
  mapping_field = field
196
155
  return self.map_by_unique_field(self.query_models(wrapper_type, field, value_list, page_limit, page_size),
197
156
  mapping_field)
198
157
 
199
- def query_models_with_criteria(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
200
- value_list: Iterable[FieldValue] | FieldValue,
158
+ def query_models_with_criteria(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
159
+ value_list: Iterable[FieldValue],
201
160
  paging_criteria: DataRecordPojoPageCriteria | None = None,
202
161
  page_limit: int | None = None) \
203
- -> tuple[list[WrappedType] | list[PyRecordModel], DataRecordPojoPageCriteria]:
162
+ -> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
204
163
  """
205
164
  Shorthand for using the data record manager to query for a list of data records by field value
206
165
  and then converting the results into a list of record models.
207
166
 
208
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
167
+ :param wrapper_type: The record model wrapper to use.
209
168
  :param field: 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.
169
+ :param value_list: The values of the field to query on.
213
170
  :param paging_criteria: The paging criteria to start the query with.
214
171
  :param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
215
172
  possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
216
173
  enforces a page size.
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.
174
+ :return: The record models for the queried records and the final paging criteria.
219
175
  """
220
- dt: str = AliasUtil.to_data_type_name(wrapper_type)
221
- if isinstance(wrapper_type, str):
222
- wrapper_type = None
176
+ dt: str = wrapper_type.get_wrapper_data_type_name()
223
177
  field: str = AliasUtil.to_data_field_name(field)
224
- if isinstance(value_list, FieldValue):
225
- value_list: list[FieldValue] = [value_list]
226
178
  pager = QueryDataRecordsAutoPager(dt, field, list(value_list), self.user, paging_criteria)
227
179
  pager.max_page = page_limit
228
180
  return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
229
181
 
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]:
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) -> list[WrappedType]:
233
184
  """
234
185
  Shorthand for using the data record manager to query for a list of data records by record ID
235
186
  and then converting the results into a list of record models.
236
187
 
237
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
188
+ :param wrapper_type: The record model wrapper to use.
238
189
  :param ids: The list of record IDs to query.
239
190
  :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
240
191
  only functions if you set a page size or the platform enforces a page size.
241
192
  :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
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.
193
+ :return: The record models for the queried records.
244
194
  """
245
195
  criteria: DataRecordPojoPageCriteria | None = None
246
196
  if page_size is not None:
247
197
  criteria = DataRecordPojoPageCriteria(page_size=page_size)
248
198
  return self.query_models_by_id_with_criteria(wrapper_type, ids, criteria, page_limit)[0]
249
199
 
250
- def query_models_by_id_with_criteria(self, wrapper_type: type[WrappedType] | str, ids: Iterable[int],
200
+ def query_models_by_id_with_criteria(self, wrapper_type: type[WrappedType], ids: Iterable[int],
251
201
  paging_criteria: DataRecordPojoPageCriteria | None = None,
252
202
  page_limit: int | None = None) \
253
- -> tuple[list[WrappedType] | list[PyRecordModel], DataRecordPojoPageCriteria]:
203
+ -> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
254
204
  """
255
205
  Shorthand for using the data record manager to query for a list of data records by record ID
256
206
  and then converting the results into a list of record models.
257
207
 
258
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
208
+ :param wrapper_type: The record model wrapper to use.
259
209
  :param ids: The list of record IDs to query.
260
210
  :param paging_criteria: The paging criteria to start the query with.
261
211
  :param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
262
212
  possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
263
213
  enforces a page size.
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.
214
+ :return: The record models for the queried records and the final paging criteria.
266
215
  """
267
- dt: str = AliasUtil.to_data_type_name(wrapper_type)
268
- if isinstance(wrapper_type, str):
269
- wrapper_type = None
216
+ dt: str = wrapper_type.get_wrapper_data_type_name()
270
217
  pager = QueryDataRecordByIdListAutoPager(dt, list(ids), self.user, paging_criteria)
271
218
  pager.max_page = page_limit
272
219
  return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
273
220
 
274
- def query_models_by_id_and_map(self, wrapper_type: type[WrappedType] | str, ids: Iterable[int],
221
+ def query_models_by_id_and_map(self, wrapper_type: type[WrappedType], ids: Iterable[int],
275
222
  page_limit: int | None = None, page_size: int | None = None) \
276
- -> dict[int, WrappedType | PyRecordModel]:
223
+ -> dict[int, WrappedType]:
277
224
  """
278
225
  Shorthand for using the data record manager to query for a list of data records by record ID
279
226
  and then converting the results into a dictionary of record ID to the record model for that ID.
280
227
 
281
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
228
+ :param wrapper_type: The record model wrapper to use.
282
229
  :param ids: The list of record IDs to query.
283
230
  :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
284
231
  only functions if you set a page size or the platform enforces a page size.
285
232
  :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
286
233
  :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.
289
234
  """
290
- return {AliasUtil.to_record_id(x): x for x in self.query_models_by_id(wrapper_type, ids, page_limit, page_size)}
235
+ return {x.record_id: x for x in self.query_models_by_id(wrapper_type, ids, page_limit, page_size)}
291
236
 
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]:
237
+ def query_all_models(self, wrapper_type: type[WrappedType], page_limit: int | None = None,
238
+ page_size: int | None = None) -> list[WrappedType]:
294
239
  """
295
240
  Shorthand for using the data record manager to query for all data records of a given type
296
241
  and then converting the results into a list of record models.
297
242
 
298
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
243
+ :param wrapper_type: The record model wrapper to use.
299
244
  :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
300
245
  only functions if you set a page size or the platform enforces a page size.
301
246
  :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
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.
247
+ :return: The record models for the queried records.
304
248
  """
305
249
  criteria: DataRecordPojoPageCriteria | None = None
306
250
  if page_size is not None:
307
251
  criteria = DataRecordPojoPageCriteria(page_size=page_size)
308
252
  return self.query_all_models_with_criteria(wrapper_type, criteria, page_limit)[0]
309
253
 
310
- def query_all_models_with_criteria(self, wrapper_type: type[WrappedType] | str,
254
+ def query_all_models_with_criteria(self, wrapper_type: type[WrappedType],
311
255
  paging_criteria: DataRecordPojoPageCriteria | None = None,
312
256
  page_limit: int | None = None) \
313
- -> tuple[list[WrappedType] | list[PyRecordModel], DataRecordPojoPageCriteria]:
257
+ -> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
314
258
  """
315
259
  Shorthand for using the data record manager to query for all data records of a given type
316
260
  and then converting the results into a list of record models.
317
261
 
318
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
262
+ :param wrapper_type: The record model wrapper to use.
319
263
  :param paging_criteria: The paging criteria to start the query with.
320
264
  :param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
321
265
  possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
322
266
  enforces a page size.
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.
267
+ :return: The record models for the queried records and the final paging criteria.
325
268
  """
326
- dt: str = AliasUtil.to_data_type_name(wrapper_type)
327
- if isinstance(wrapper_type, str):
328
- wrapper_type = None
269
+ dt: str = wrapper_type.get_wrapper_data_type_name()
329
270
  pager = QueryAllRecordsOfTypeAutoPager(dt, self.user, paging_criteria)
330
271
  pager.max_page = page_limit
331
272
  return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
332
273
 
333
- def query_models_by_report(self, wrapper_type: type[WrappedType] | str,
274
+ def query_models_by_report(self, wrapper_type: type[WrappedType],
334
275
  report_name: str | RawReportTerm | CustomReportCriteria,
335
276
  filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
336
277
  page_limit: int | None = None,
337
278
  page_size: int | None = None,
338
- page_number: int | None = None) -> list[WrappedType] | list[PyRecordModel]:
279
+ page_number: int | None = None) -> list[WrappedType]:
339
280
  """
340
281
  Run a report and use the results of that report to query for and return the records in the report results.
341
282
  First runs the report, then runs a data record manager query on the results of the custom report.
@@ -345,7 +286,7 @@ class RecordHandler:
345
286
 
346
287
  Any given custom report criteria should only have columns from a single data type.
347
288
 
348
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
289
+ :param wrapper_type: The record model wrapper to use.
349
290
  :param report_name: The name of a system report, or a raw report term for a quick report, or custom report
350
291
  criteria for a custom report.
351
292
  :param filters: If provided, filter the results of the report using the given mapping of headers to values to
@@ -357,10 +298,8 @@ class RecordHandler:
357
298
  :param page_number: The page number to start the search from, If None, starts on the first page.
358
299
  If the input report is a custom report criteria, uses the value from the criteria, unless this value is
359
300
  not None, in which case it overwrites the given report's value. Note that the number of the first page is 0.
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.
301
+ :return: The record models for the queried records that matched the given report.
362
302
  """
363
- warnings.warn("Deprecated in favor of the [System/Custom/Quick]ReportRecordAutoPager classes.", DeprecationWarning)
364
303
  if isinstance(report_name, str):
365
304
  results: list[dict[str, FieldValue]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
366
305
  page_limit, page_size, page_number)
@@ -368,7 +307,7 @@ class RecordHandler:
368
307
  results: list[dict[str, FieldValue]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
369
308
  page_limit, page_size, page_number)
370
309
  elif isinstance(report_name, CustomReportCriteria):
371
- dt: str = AliasUtil.to_data_type_name(wrapper_type)
310
+ dt: str = wrapper_type.get_wrapper_data_type_name()
372
311
  # Ensure that the root data type is the one we're looking for.
373
312
  report_name.root_data_type = dt
374
313
  # Raise an exception if any column in the report doesn't match the given data type.
@@ -383,45 +322,40 @@ class RecordHandler:
383
322
  raise SapioException("Unrecognized report object.")
384
323
 
385
324
  # Using the bracket accessor because we want to throw an exception if RecordId doesn't exist in the report.
386
- # This should only possibly be the case with system reports, as quick reports will include the record ID, and
325
+ # This should only possibly be the case with system reports, as quick reports will include the record ID and
387
326
  # we forced any given custom report to have a record ID column.
388
327
  ids: list[int] = [row["RecordId"] for row in results]
389
328
  return self.query_models_by_id(wrapper_type, ids)
390
329
 
391
- def add_model(self, wrapper_type: type[WrappedType] | str) -> WrappedType | PyRecordModel:
330
+ def add_model(self, wrapper_type: type[WrappedType]) -> WrappedType:
392
331
  """
393
332
  Shorthand for using the instance manager to add a new record model of the given type.
394
333
 
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.
334
+ :param wrapper_type: The record model wrapper to use.
335
+ :return: The newly added record model.
398
336
  """
399
- return self.add_models(wrapper_type, 1)[0]
337
+ return self.inst_man.add_new_record_of_type(wrapper_type)
400
338
 
401
- def add_models(self, wrapper_type: type[WrappedType] | str, num: int) -> list[WrappedType] | list[PyRecordModel]:
339
+ def add_models(self, wrapper_type: type[WrappedType], num: int) -> list[WrappedType]:
402
340
  """
403
341
  Shorthand for using the instance manager to add new record models of the given type.
404
342
 
405
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
343
+ :param wrapper_type: The record model wrapper to use.
406
344
  :param num: The number of models to create.
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.
345
+ :return: The newly added record models.
409
346
  """
410
- if isinstance(wrapper_type, str):
411
- return self.inst_man.add_new_records(wrapper_type, num)
412
347
  return self.inst_man.add_new_records_of_type(num, wrapper_type)
413
348
 
414
- def add_models_with_data(self, wrapper_type: type[WrappedType] | str, fields: list[FieldIdentifierMap]) \
415
- -> list[WrappedType] | list[PyRecordModel]:
349
+ def add_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldIdentifierMap]) \
350
+ -> list[WrappedType]:
416
351
  """
417
352
  Shorthand for using the instance manager to add new models of the given type, and then initializing all those
418
353
  models with the given fields.
419
354
 
420
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
355
+ :param wrapper_type: The record model wrapper to use.
421
356
  :param fields: A list of field maps to initialize the record models with.
422
357
  :return: The newly added record models with the provided fields set. The records will be in the same order as
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.
358
+ the fields in the fields list.
425
359
  """
426
360
  fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
427
361
  models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
@@ -429,9 +363,8 @@ class RecordHandler:
429
363
  model.set_field_values(field_list)
430
364
  return models
431
365
 
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:
366
+ def find_or_add_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
367
+ id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType:
435
368
  """
436
369
  Find a unique record that matches the given field values. If no such records exist, add a record model to the
437
370
  cache with the identifying fields set to the desired values. This record will be created in the system when
@@ -442,14 +375,12 @@ class RecordHandler:
442
375
 
443
376
  Makes a webservice call to query for the existing record.
444
377
 
445
- :param wrapper_type: The record model wrapper to use, or the data type name of the record.
378
+ :param wrapper_type: The record model wrapper to use.
446
379
  :param primary_identifier: The data field name of the field to search on.
447
380
  :param id_value: The value of the identifying field to search for.
448
381
  :param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
449
382
  the primary identifier.
450
383
  :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.
453
384
  """
454
385
  # PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
455
386
  # If no secondary identifiers were provided, use an empty dictionary.
@@ -470,25 +401,22 @@ class RecordHandler:
470
401
  secondary_identifiers.update({primary_identifier: id_value})
471
402
  return self.add_models_with_data(wrapper_type, [secondary_identifiers])[0]
472
403
 
473
- def create_models(self, wrapper_type: type[WrappedType] | str, num: int) -> list[WrappedType] | list[PyRecordModel]:
404
+ def create_models(self, wrapper_type: type[WrappedType], num: int) -> list[WrappedType]:
474
405
  """
475
406
  Shorthand for creating new records via the data record manager and then returning them as wrapped
476
407
  record models. Useful in cases where your record model needs to have a valid record ID.
477
408
 
478
409
  Makes a webservice call to create the data records.
479
410
 
480
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
411
+ :param wrapper_type: The record model wrapper to use.
481
412
  :param num: The number of new records to create.
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.
413
+ :return: The newly created record models.
484
414
  """
485
- dt: str = AliasUtil.to_data_type_name(wrapper_type)
486
- if isinstance(wrapper_type, str):
487
- wrapper_type = None
415
+ dt: str = wrapper_type.get_wrapper_data_type_name()
488
416
  return self.wrap_models(self.dr_man.add_data_records(dt, num), wrapper_type)
489
417
 
490
- def create_models_with_data(self, wrapper_type: type[WrappedType] | str, fields: list[FieldIdentifierMap]) \
491
- -> list[WrappedType] | list[PyRecordModel]:
418
+ def create_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldIdentifierMap]) \
419
+ -> list[WrappedType]:
492
420
  """
493
421
  Shorthand for creating new records via the data record manager with field data to initialize the records with
494
422
  and then returning them as wrapped record models. Useful in cases where your record model needs to have a valid
@@ -496,20 +424,17 @@ class RecordHandler:
496
424
 
497
425
  Makes a webservice call to create the data records.
498
426
 
499
- :param wrapper_type: The record model wrapper to use, or the data type name of the records.
427
+ :param wrapper_type: The record model wrapper to use.
500
428
  :param fields: The field map list to initialize the new data records with.
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.
429
+ :return: The newly created record models.
503
430
  """
504
- dt: str = AliasUtil.to_data_type_name(wrapper_type)
505
- if isinstance(wrapper_type, str):
506
- wrapper_type = None
431
+ dt: str = wrapper_type.get_wrapper_data_type_name()
507
432
  fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
508
433
  return self.wrap_models(self.dr_man.add_data_records_with_data(dt, fields), wrapper_type)
509
434
 
510
- def find_or_create_model(self, wrapper_type: type[WrappedType] | str, primary_identifier: FieldIdentifier,
435
+ def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
511
436
  id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
512
- -> WrappedType | PyRecordModel:
437
+ -> WrappedType:
513
438
  """
514
439
  Find a unique record that matches the given field values. If no such records exist, create one with the
515
440
  identifying fields set to the desired values. If more than one record with the identifying values exists,
@@ -521,14 +446,12 @@ class RecordHandler:
521
446
  Makes a webservice call to query for the existing record. Makes an additional webservice call if the record
522
447
  needs to be created.
523
448
 
524
- :param wrapper_type: The record model wrapper to use, or the data type name of the record.
449
+ :param wrapper_type: The record model wrapper to use.
525
450
  :param primary_identifier: The data field name of the field to search on.
526
451
  :param id_value: The value of the identifying field to search for.
527
452
  :param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
528
453
  the primary identifier.
529
454
  :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.
532
455
  """
533
456
  # PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
534
457
  # If no secondary identifiers were provided, use an empty dictionary.
@@ -549,281 +472,8 @@ class RecordHandler:
549
472
  secondary_identifiers.update({primary_identifier: id_value})
550
473
  return self.create_models_with_data(wrapper_type, [secondary_identifiers])[0]
551
474
 
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
-
824
475
  @staticmethod
825
- def map_to_parent(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType])\
826
- -> dict[WrappedRecordModel, WrappedType]:
476
+ def map_to_parent(models: Iterable[RecordModel], parent_type: type[WrappedType]) -> dict[RecordModel, WrappedType]:
827
477
  """
828
478
  Map a list of record models to a single parent of a given type. The parents must already be loaded.
829
479
 
@@ -832,14 +482,14 @@ class RecordHandler:
832
482
  :return: A dict[ModelType, ParentType]. If an input model doesn't have a parent of the given parent type, then
833
483
  it will map to None.
834
484
  """
835
- return_dict: dict[WrappedRecordModel, WrappedType] = {}
485
+ return_dict: dict[RecordModel, WrappedType] = {}
836
486
  for model in models:
837
487
  return_dict[model] = model.get_parent_of_type(parent_type)
838
488
  return return_dict
839
489
 
840
490
  @staticmethod
841
- def map_to_parents(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
842
- -> dict[WrappedRecordModel, list[WrappedType]]:
491
+ def map_to_parents(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
492
+ -> dict[RecordModel, list[WrappedType]]:
843
493
  """
844
494
  Map a list of record models to a list parents of a given type. The parents must already be loaded.
845
495
 
@@ -848,14 +498,14 @@ class RecordHandler:
848
498
  :return: A dict[ModelType, list[ParentType]]. If an input model doesn't have a parent of the given parent type,
849
499
  then it will map to an empty list.
850
500
  """
851
- return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
501
+ return_dict: dict[RecordModel, list[WrappedType]] = {}
852
502
  for model in models:
853
503
  return_dict[model] = model.get_parents_of_type(parent_type)
854
504
  return return_dict
855
505
 
856
506
  @staticmethod
857
- def map_by_parent(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
858
- -> dict[WrappedType, WrappedRecordModel]:
507
+ def map_by_parent(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
508
+ -> dict[WrappedType, RecordModel]:
859
509
  """
860
510
  Take a list of record models and map them by their parent. Essentially an inversion of map_to_parent.
861
511
  If two records share the same parent, an exception will be thrown. The parents must already be loaded.
@@ -865,8 +515,8 @@ class RecordHandler:
865
515
  :return: A dict[ParentType, ModelType]. If an input model doesn't have a parent of the given parent type,
866
516
  then it will not be in the resulting dictionary.
867
517
  """
868
- to_parent: dict[WrappedRecordModel, WrappedType] = RecordHandler.map_to_parent(models, parent_type)
869
- by_parent: dict[WrappedType, WrappedRecordModel] = {}
518
+ to_parent: dict[RecordModel, WrappedType] = RecordHandler.map_to_parent(models, parent_type)
519
+ by_parent: dict[WrappedType, RecordModel] = {}
870
520
  for record, parent in to_parent.items():
871
521
  if parent is None:
872
522
  continue
@@ -877,8 +527,8 @@ class RecordHandler:
877
527
  return by_parent
878
528
 
879
529
  @staticmethod
880
- def map_by_parents(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
881
- -> dict[WrappedType, list[WrappedRecordModel]]:
530
+ def map_by_parents(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
531
+ -> dict[WrappedType, list[RecordModel]]:
882
532
  """
883
533
  Take a list of record models and map them by their parents. Essentially an inversion of map_to_parents. Input
884
534
  models that share a parent will end up in the same list. The parents must already be loaded.
@@ -888,16 +538,15 @@ class RecordHandler:
888
538
  :return: A dict[ParentType, list[ModelType]]. If an input model doesn't have a parent of the given parent type,
889
539
  then it will not be in the resulting dictionary.
890
540
  """
891
- to_parents: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler.map_to_parents(models, parent_type)
892
- by_parents: dict[WrappedType, list[WrappedRecordModel]] = {}
541
+ to_parents: dict[RecordModel, list[WrappedType]] = RecordHandler.map_to_parents(models, parent_type)
542
+ by_parents: dict[WrappedType, list[RecordModel]] = {}
893
543
  for record, parents in to_parents.items():
894
544
  for parent in parents:
895
545
  by_parents.setdefault(parent, []).append(record)
896
546
  return by_parents
897
547
 
898
548
  @staticmethod
899
- def map_to_child(models: Iterable[WrappedRecordModel], child_type: type[WrappedType])\
900
- -> dict[WrappedRecordModel, WrappedType]:
549
+ def map_to_child(models: Iterable[RecordModel], child_type: type[WrappedType]) -> dict[RecordModel, WrappedType]:
901
550
  """
902
551
  Map a list of record models to a single child of a given type. The children must already be loaded.
903
552
 
@@ -906,14 +555,14 @@ class RecordHandler:
906
555
  :return: A dict[ModelType, ChildType]. If an input model doesn't have a child of the given child type, then
907
556
  it will map to None.
908
557
  """
909
- return_dict: dict[WrappedRecordModel, WrappedType] = {}
558
+ return_dict: dict[RecordModel, WrappedType] = {}
910
559
  for model in models:
911
560
  return_dict[model] = model.get_child_of_type(child_type)
912
561
  return return_dict
913
562
 
914
563
  @staticmethod
915
- def map_to_children(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
916
- -> dict[WrappedRecordModel, list[WrappedType]]:
564
+ def map_to_children(models: Iterable[RecordModel], child_type: type[WrappedType]) \
565
+ -> dict[RecordModel, list[WrappedType]]:
917
566
  """
918
567
  Map a list of record models to a list children of a given type. The children must already be loaded.
919
568
 
@@ -922,14 +571,14 @@ class RecordHandler:
922
571
  :return: A dict[ModelType, list[ChildType]]. If an input model doesn't have children of the given child type,
923
572
  then it will map to an empty list.
924
573
  """
925
- return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
574
+ return_dict: dict[RecordModel, list[WrappedType]] = {}
926
575
  for model in models:
927
576
  return_dict[model] = model.get_children_of_type(child_type)
928
577
  return return_dict
929
578
 
930
579
  @staticmethod
931
- def map_by_child(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
932
- -> dict[WrappedType, WrappedRecordModel]:
580
+ def map_by_child(models: Iterable[RecordModel], child_type: type[WrappedType]) \
581
+ -> dict[WrappedType, RecordModel]:
933
582
  """
934
583
  Take a list of record models and map them by their children. Essentially an inversion of map_to_child.
935
584
  If two records share the same child, an exception will be thrown. The children must already be loaded.
@@ -939,8 +588,8 @@ class RecordHandler:
939
588
  :return: A dict[ChildType, ModelType]. If an input model doesn't have a child of the given child type,
940
589
  then it will not be in the resulting dictionary.
941
590
  """
942
- to_child: dict[WrappedRecordModel, WrappedType] = RecordHandler.map_to_child(models, child_type)
943
- by_child: dict[WrappedType, WrappedRecordModel] = {}
591
+ to_child: dict[RecordModel, WrappedType] = RecordHandler.map_to_child(models, child_type)
592
+ by_child: dict[WrappedType, RecordModel] = {}
944
593
  for record, child in to_child.items():
945
594
  if child is None:
946
595
  continue
@@ -951,8 +600,8 @@ class RecordHandler:
951
600
  return by_child
952
601
 
953
602
  @staticmethod
954
- def map_by_children(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
955
- -> dict[WrappedType, list[WrappedRecordModel]]:
603
+ def map_by_children(models: Iterable[RecordModel], child_type: type[WrappedType]) \
604
+ -> dict[WrappedType, list[RecordModel]]:
956
605
  """
957
606
  Take a list of record models and map them by their children. Essentially an inversion of map_to_children. Input
958
607
  models that share a child will end up in the same list. The children must already be loaded.
@@ -962,8 +611,8 @@ class RecordHandler:
962
611
  :return: A dict[ChildType, list[ModelType]]. If an input model doesn't have children of the given child type,
963
612
  then it will not be in the resulting dictionary.
964
613
  """
965
- to_children: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler.map_to_children(models, child_type)
966
- by_children: dict[WrappedType, list[WrappedRecordModel]] = {}
614
+ to_children: dict[RecordModel, list[WrappedType]] = RecordHandler.map_to_children(models, child_type)
615
+ by_children: dict[WrappedType, list[RecordModel]] = {}
967
616
  for record, children in to_children.items():
968
617
  for child in children:
969
618
  by_children.setdefault(child, []).append(record)
@@ -1202,7 +851,7 @@ class RecordHandler:
1202
851
  return field_sum
1203
852
 
1204
853
  @staticmethod
1205
- def mean_of_field(models: Collection[SapioRecord], field_name: FieldIdentifier) -> float:
854
+ def mean_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
1206
855
  """
1207
856
  Calculate the mean of the numeric value of a given field across all input models. Excepts that all given models
1208
857
  have a value. If the field is an integer field, the value will be converted to a float.
@@ -1211,7 +860,7 @@ class RecordHandler:
1211
860
  :param field_name: The name of the numeric field to mean.
1212
861
  :return: The mean of the field values for the collection of models.
1213
862
  """
1214
- return RecordHandler.sum_of_field(models, field_name) / len(models)
863
+ return RecordHandler.sum_of_field(models, field_name) / len(list(models))
1215
864
 
1216
865
  @staticmethod
1217
866
  def get_newest_record(records: Iterable[SapioRecord]) -> SapioRecord:
@@ -1221,7 +870,11 @@ class RecordHandler:
1221
870
  :param records: The list of records.
1222
871
  :return: The input record with the highest record ID. None if the input list is empty.
1223
872
  """
1224
- return max(records, key=lambda x: x.record_id)
873
+ newest: SapioRecord | None = None
874
+ for record in records:
875
+ if newest is None or record.record_id > newest.record_id:
876
+ newest = record
877
+ return newest
1225
878
 
1226
879
  # FR-46696: Add a function for getting the oldest record in a list, just like we have one for the newest record.
1227
880
  @staticmethod
@@ -1232,18 +885,22 @@ class RecordHandler:
1232
885
  :param records: The list of records.
1233
886
  :return: The input record with the lowest record ID. None if the input list is empty.
1234
887
  """
1235
- return min(records, key=lambda x: x.record_id)
888
+ oldest: SapioRecord | None = None
889
+ for record in records:
890
+ if oldest is None or record.record_id < oldest.record_id:
891
+ oldest = record
892
+ return oldest
1236
893
 
1237
894
  @staticmethod
1238
895
  def values_to_field_maps(field_name: FieldIdentifier, values: Iterable[FieldValue],
1239
- existing_fields: list[FieldMap] | None = None) -> list[FieldMap]:
896
+ existing_fields: list[FieldIdentifier] | None = None) -> list[FieldMap]:
1240
897
  """
1241
898
  Add a list of values for a specific field to a list of dictionaries pairing each value to that field name.
1242
899
 
1243
900
  :param field_name: The name of the field that the values are from.
1244
901
  :param values: A list of field values.
1245
902
  :param existing_fields: An optional existing fields map list to add the new values to. Values are added in the
1246
- list in the same order that they appear. If no existing fields are provided, returns a new fields map list.
903
+ list in the same order that they appear. If no existing fields are provided, returns a new fields map list.
1247
904
  :return: A fields map list that contains the given values mapped by the given field name.
1248
905
  """
1249
906
  # Update the existing fields map list if one is given.
@@ -1262,9 +919,8 @@ class RecordHandler:
1262
919
 
1263
920
  # FR-46155: Update relationship path traversing functions to be non-static and take in a wrapper type so that the
1264
921
  # output can be wrapped instead of requiring the user to wrap the output.
1265
- def get_linear_path(self, models: Iterable[RecordModel], path: RelationshipPath,
1266
- wrapper_type: type[WrappedType] | None = None) \
1267
- -> dict[RecordModel, WrappedType | PyRecordModel | None]:
922
+ def get_linear_path(self, models: Iterable[RecordModel], path: RelationshipPath, wrapper_type: type[WrappedType]) \
923
+ -> dict[RecordModel, WrappedType | None]:
1268
924
  """
1269
925
  Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
1270
926
  path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
@@ -1272,8 +928,7 @@ class RecordHandler:
1272
928
 
1273
929
  :param models: A list of record models.
1274
930
  :param path: The relationship path to follow.
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.
931
+ :param wrapper_type: The record model wrapper to use.
1277
932
  :return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
1278
933
  path couldn't be reached, the record will map to None.
1279
934
  """
@@ -1321,12 +976,11 @@ class RecordHandler:
1321
976
  current = reverse_links[0]
1322
977
  else:
1323
978
  raise SapioException("Unsupported path direction.")
1324
- ret_dict.update({model: self.wrap_model(current, wrapper_type) if current else None})
979
+ ret_dict.update({model: self.inst_man.wrap(current, wrapper_type) if current else None})
1325
980
  return ret_dict
1326
981
 
1327
982
  def get_branching_path(self, models: Iterable[RecordModel], path: RelationshipPath,
1328
- wrapper_type: type[WrappedType] | None = None)\
1329
- -> dict[RecordModel, list[WrappedType] | list[PyRecordModel]]:
983
+ wrapper_type: type[WrappedType]) -> dict[RecordModel, list[WrappedType]]:
1330
984
  """
1331
985
  Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
1332
986
  path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
@@ -1334,8 +988,7 @@ class RecordHandler:
1334
988
 
1335
989
  :param models: A list of record models.
1336
990
  :param path: The relationship path to follow.
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.
991
+ :param wrapper_type: The record model wrapper to use.
1339
992
  :return: Each record model mapped to the records at the end of the path starting from itself. If the end of the
1340
993
  path couldn't be reached, the record will map to an empty list.
1341
994
  """
@@ -1368,14 +1021,13 @@ class RecordHandler:
1368
1021
  raise SapioException("Unsupported path direction.")
1369
1022
  current_search = next_search
1370
1023
  next_search = set()
1371
- ret_dict.update({model: self.wrap_models(current_search, wrapper_type)})
1024
+ ret_dict.update({model: self.inst_man.wrap_list(list(current_search), wrapper_type)})
1372
1025
  return ret_dict
1373
1026
 
1374
1027
  # FR-46155: Create a relationship traversing function that returns a single function at the end of the path like
1375
1028
  # get_linear_path but can handle branching paths in the middle of the search like get_branching_path.
1376
- def get_flat_path(self, models: Iterable[RecordModel], path: RelationshipPath,
1377
- wrapper_type: type[WrappedType] | None = None) \
1378
- -> dict[RecordModel, WrappedType | PyRecordModel | None]:
1029
+ def get_flat_path(self, models: Iterable[RecordModel], path: RelationshipPath, wrapper_type: type[WrappedType]) \
1030
+ -> dict[RecordModel, WrappedType | None]:
1379
1031
  """
1380
1032
  Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
1381
1033
  path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
@@ -1387,8 +1039,7 @@ class RecordHandler:
1387
1039
 
1388
1040
  :param models: A list of record models.
1389
1041
  :param path: The relationship path to follow.
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.
1042
+ :param wrapper_type: The record model wrapper to use.
1392
1043
  :return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
1393
1044
  path couldn't be reached, the record will map to None.
1394
1045
  """
@@ -1416,22 +1067,21 @@ class RecordHandler:
1416
1067
  current = current[0].get_reverse_side_link(data_type, node.data_field_name)
1417
1068
  else:
1418
1069
  raise SapioException("Unsupported path direction.")
1419
- ret_dict.update({model: self.wrap_model(current[0], wrapper_type) if current else None})
1070
+ ret_dict.update({model: self.inst_man.wrap(current[0], wrapper_type) if current else None})
1420
1071
  return ret_dict
1421
1072
 
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:
1073
+ def __find_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value: FieldValue,
1074
+ secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType | None:
1424
1075
  """
1425
1076
  Find a record from the system that matches the given field values. The primary identifier and value is used
1426
1077
  to query for the record, then the secondary identifiers may be optionally provided to further filter the
1427
1078
  returned results. If no record is found with these filters, returns None.
1428
1079
  """
1429
1080
  # Query for all records that match the primary identifier.
1430
- results: list[WrappedType] | list[PyRecordModel] = self.query_models(wrapper_type, primary_identifier,
1431
- [id_value])
1081
+ results: list[WrappedType] = self.query_models(wrapper_type, primary_identifier, [id_value])
1432
1082
 
1433
1083
  # Find the one record, if any, that matches the secondary identifiers.
1434
- unique_record: WrappedType | PyRecordModel | None = None
1084
+ unique_record: WrappedType | None = None
1435
1085
  for result in results:
1436
1086
  matches_all: bool = True
1437
1087
  for field, value in secondary_identifiers.items():
@@ -1441,60 +1091,22 @@ class RecordHandler:
1441
1091
  if matches_all:
1442
1092
  # If a previous record in the results already matched all identifiers, then throw an exception.
1443
1093
  if unique_record is not None:
1444
- raise SapioException(f"More than one record of type {AliasUtil.to_data_type_name(wrapper_type)} "
1094
+ raise SapioException(f"More than one record of type {wrapper_type.get_wrapper_data_type_name()} "
1445
1095
  f"encountered in system that matches all provided identifiers.")
1446
1096
  unique_record = result
1447
1097
  return unique_record
1448
1098
 
1449
1099
  @staticmethod
1450
- def __verify_data_type(record: DataRecord | PyRecordModel, wrapper_type: type[WrappedType]) -> None:
1100
+ def __verify_data_type(records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> None:
1451
1101
  """
1452
- Throw an exception if the data type of the given record and wrapper don't match.
1102
+ Throw an exception if the data type of the given records and wrapper don't match.
1453
1103
  """
1454
1104
  model_type: str = wrapper_type.get_wrapper_data_type_name()
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))
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}")