sapiopycommons 2025.7.18a620__py3-none-any.whl → 2025.7.21a630__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/ai/api/plan/tool/proto/tool_pb2.py +16 -14
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2.pyi +22 -4
- sapiopycommons/ai/protobuf_utils.py +1 -0
- sapiopycommons/ai/test_client.py +1 -0
- sapiopycommons/ai/tool_service_base.py +143 -66
- sapiopycommons/callbacks/callback_util.py +665 -332
- sapiopycommons/callbacks/field_builder.py +2 -0
- sapiopycommons/chem/IndigoMolecules.py +29 -1
- sapiopycommons/chem/Molecules.py +3 -3
- sapiopycommons/customreport/auto_pagers.py +26 -1
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/pseudo_data_types.py +349 -326
- sapiopycommons/eln/experiment_cache.py +188 -0
- sapiopycommons/eln/experiment_handler.py +408 -767
- sapiopycommons/eln/experiment_report_util.py +11 -6
- sapiopycommons/eln/experiment_step_factory.py +476 -0
- sapiopycommons/eln/plate_designer.py +7 -2
- sapiopycommons/eln/step_creation.py +236 -0
- sapiopycommons/files/file_util.py +7 -5
- sapiopycommons/general/accession_service.py +2 -2
- sapiopycommons/general/aliases.py +3 -1
- sapiopycommons/general/audit_log.py +7 -0
- sapiopycommons/general/custom_report_util.py +12 -0
- sapiopycommons/general/data_structure_util.py +115 -0
- sapiopycommons/processtracking/custom_workflow_handler.py +11 -1
- sapiopycommons/processtracking/endpoints.py +27 -0
- sapiopycommons/recordmodel/record_handler.py +783 -389
- sapiopycommons/rules/eln_rule_handler.py +8 -1
- sapiopycommons/rules/on_save_rule_handler.py +8 -1
- sapiopycommons/webhook/webhook_handlers.py +9 -4
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.7.18a620.dist-info → sapiopycommons-2025.7.21a630.dist-info}/METADATA +2 -2
- {sapiopycommons-2025.7.18a620.dist-info → sapiopycommons-2025.7.21a630.dist-info}/RECORD +35 -31
- {sapiopycommons-2025.7.18a620.dist-info → sapiopycommons-2025.7.21a630.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.7.18a620.dist-info → sapiopycommons-2025.7.21a630.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import io
|
|
3
4
|
import warnings
|
|
4
5
|
from collections.abc import Iterable
|
|
6
|
+
from typing import Collection, TypeVar, TypeAlias
|
|
5
7
|
from weakref import WeakValueDictionary
|
|
6
8
|
|
|
9
|
+
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
|
|
10
|
+
FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey, DataTypeIdentifier
|
|
11
|
+
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
12
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
7
13
|
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
8
14
|
from sapiopylib.rest.User import SapioUser
|
|
9
15
|
from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, RawReportTerm, ReportColumn
|
|
@@ -13,21 +19,35 @@ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
|
13
19
|
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
14
20
|
from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
|
|
15
21
|
QueryAllRecordsOfTypeAutoPager
|
|
16
|
-
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
22
|
+
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel, AbstractRecordModelPropertyGetter, \
|
|
23
|
+
RecordModelPropertyType, AbstractRecordModelPropertyAdder, AbstractRecordModelPropertySetter, \
|
|
24
|
+
AbstractRecordModelPropertyRemover
|
|
17
25
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager, \
|
|
18
26
|
RecordModelRelationshipManager
|
|
19
27
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, WrappedRecordModel
|
|
20
28
|
from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
|
|
21
29
|
RelationshipNodeType
|
|
22
30
|
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
31
|
+
from sapiopylib.rest.utils.recordmodel.properties import Parents, Parent, Children, Child, ForwardSideLink, \
|
|
32
|
+
ReverseSideLink
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
# Aliases for longer name.
|
|
35
|
+
_PropertyGetter: TypeAlias = AbstractRecordModelPropertyGetter
|
|
36
|
+
_PropertyAdder: TypeAlias = AbstractRecordModelPropertyAdder
|
|
37
|
+
_PropertyRemover: TypeAlias = AbstractRecordModelPropertyRemover
|
|
38
|
+
_PropertySetter: TypeAlias = AbstractRecordModelPropertySetter
|
|
39
|
+
_PropertyType: TypeAlias = RecordModelPropertyType
|
|
40
|
+
|
|
41
|
+
# CR-47717: Use TypeVars in the type hints of certain functions to prevent PyCharm from erroneously flagging certain
|
|
42
|
+
# return type hints as incorrect.
|
|
43
|
+
IsRecordModel = TypeVar('IsRecordModel', bound=RecordModel)
|
|
44
|
+
"""A PyRecordModel or AbstractRecordModel."""
|
|
45
|
+
IsSapioRecord = TypeVar('IsSapioRecord', bound=SapioRecord)
|
|
46
|
+
"""A DataRecord, PyRecordModel, or AbstractRecordModel."""
|
|
28
47
|
|
|
29
48
|
|
|
30
49
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
50
|
+
# FR-47575 - Reordered functions so that the Java and Python versions are as close to each other as possible.
|
|
31
51
|
class RecordHandler:
|
|
32
52
|
"""
|
|
33
53
|
A collection of shorthand methods for dealing with the various record managers.
|
|
@@ -58,12 +78,11 @@ class RecordHandler:
|
|
|
58
78
|
"""
|
|
59
79
|
:param context: The current webhook context or a user object to send requests from.
|
|
60
80
|
"""
|
|
61
|
-
self.user = AliasUtil.to_sapio_user(context)
|
|
62
81
|
if self.__initialized:
|
|
63
82
|
return
|
|
64
83
|
self.__initialized = True
|
|
65
84
|
|
|
66
|
-
self.user =
|
|
85
|
+
self.user = AliasUtil.to_sapio_user(context)
|
|
67
86
|
self.dr_man = DataRecordManager(self.user)
|
|
68
87
|
self.rec_man = RecordModelManager(self.user)
|
|
69
88
|
self.inst_man = self.rec_man.instance_manager
|
|
@@ -103,9 +122,172 @@ class RecordHandler:
|
|
|
103
122
|
"""
|
|
104
123
|
return [self.wrap_model(x, wrapper_type) for x in records]
|
|
105
124
|
|
|
125
|
+
def add_model(self, wrapper_type: type[WrappedType] | str) -> WrappedType | PyRecordModel:
|
|
126
|
+
"""
|
|
127
|
+
Shorthand for using the instance manager to add a new record model of the given type.
|
|
128
|
+
|
|
129
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the record.
|
|
130
|
+
:return: The newly added record model. If a data type name was used instead of a model wrapper, then the
|
|
131
|
+
returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
132
|
+
"""
|
|
133
|
+
return self.add_models(wrapper_type, 1)[0]
|
|
134
|
+
|
|
135
|
+
def add_models(self, wrapper_type: type[WrappedType] | str, num: int) -> list[WrappedType] | list[PyRecordModel]:
|
|
136
|
+
"""
|
|
137
|
+
Shorthand for using the instance manager to add new record models of the given type.
|
|
138
|
+
|
|
139
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
140
|
+
:param num: The number of models to create.
|
|
141
|
+
:return: The newly added record models. If a data type name was used instead of a model wrapper, then the
|
|
142
|
+
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
143
|
+
"""
|
|
144
|
+
if isinstance(wrapper_type, str):
|
|
145
|
+
return self.inst_man.add_new_records(wrapper_type, num)
|
|
146
|
+
return self.inst_man.add_new_records_of_type(num, wrapper_type)
|
|
147
|
+
|
|
148
|
+
def add_models_with_data(self, wrapper_type: type[WrappedType] | str, fields: list[FieldIdentifierMap]) \
|
|
149
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
150
|
+
"""
|
|
151
|
+
Shorthand for using the instance manager to add new models of the given type, and then initializing all those
|
|
152
|
+
models with the given fields.
|
|
153
|
+
|
|
154
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
155
|
+
:param fields: A list of field maps to initialize the record models with.
|
|
156
|
+
:return: The newly added record models with the provided fields set. The records will be in the same order as
|
|
157
|
+
the fields in the fields list. If a data type name was used instead of a model wrapper, then the returned
|
|
158
|
+
records will be PyRecordModels instead of WrappedRecordModels.
|
|
159
|
+
"""
|
|
160
|
+
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
161
|
+
models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
|
|
162
|
+
for model, field_list in zip(models, fields):
|
|
163
|
+
model.set_field_values(field_list)
|
|
164
|
+
return models
|
|
165
|
+
|
|
166
|
+
def find_or_add_model(self, wrapper_type: type[WrappedType] | str, primary_identifier: FieldIdentifier,
|
|
167
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
|
|
168
|
+
-> WrappedType | PyRecordModel:
|
|
169
|
+
"""
|
|
170
|
+
Find a unique record that matches the given field values. If no such records exist, add a record model to the
|
|
171
|
+
cache with the identifying fields set to the desired values. This record will be created in the system when
|
|
172
|
+
you store and commit changes. If more than one record with the identifying values exists, throws an exception.
|
|
173
|
+
|
|
174
|
+
The record is searched for using the primary identifier field name and value. If multiple records are returned
|
|
175
|
+
by the query on this primary identifier, then the secondary identifiers are used to filter the results.
|
|
176
|
+
|
|
177
|
+
Makes a webservice call to query for the existing record.
|
|
178
|
+
|
|
179
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the record.
|
|
180
|
+
:param primary_identifier: The data field name of the field to search on.
|
|
181
|
+
:param id_value: The value of the identifying field to search for.
|
|
182
|
+
:param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
|
|
183
|
+
the primary identifier.
|
|
184
|
+
:return: The record model with the identifying field value, either pulled from the system or newly created.
|
|
185
|
+
If a data type name was used instead of a model wrapper, then the returned record will be a PyRecordModel
|
|
186
|
+
instead of a WrappedRecordModel.
|
|
187
|
+
"""
|
|
188
|
+
# PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
|
|
189
|
+
# If no secondary identifiers were provided, use an empty dictionary.
|
|
190
|
+
if secondary_identifiers is None:
|
|
191
|
+
secondary_identifiers = {}
|
|
192
|
+
|
|
193
|
+
primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
|
|
194
|
+
secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
|
|
195
|
+
unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
|
|
196
|
+
secondary_identifiers)
|
|
197
|
+
# If a unique record matched the identifiers, return it.
|
|
198
|
+
if unique_record is not None:
|
|
199
|
+
return unique_record
|
|
200
|
+
|
|
201
|
+
# If none of the results matched the identifiers, create a new record with all identifiers set.
|
|
202
|
+
# Put the primary identifier and value into the secondary identifiers list and use that as the fields map
|
|
203
|
+
# for this new record.
|
|
204
|
+
secondary_identifiers.update({primary_identifier: id_value})
|
|
205
|
+
return self.add_models_with_data(wrapper_type, [secondary_identifiers])[0]
|
|
206
|
+
|
|
207
|
+
def create_models(self, wrapper_type: type[WrappedType] | str, num: int) -> list[WrappedType] | list[PyRecordModel]:
|
|
208
|
+
"""
|
|
209
|
+
Shorthand for creating new records via the data record manager and then returning them as wrapped
|
|
210
|
+
record models. Useful in cases where your record model needs to have a valid record ID.
|
|
211
|
+
|
|
212
|
+
Makes a webservice call to create the data records.
|
|
213
|
+
|
|
214
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
215
|
+
:param num: The number of new records to create.
|
|
216
|
+
:return: The newly created record models. If a data type name was used instead of a model wrapper, then the
|
|
217
|
+
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
218
|
+
"""
|
|
219
|
+
dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
220
|
+
if isinstance(wrapper_type, str):
|
|
221
|
+
wrapper_type = None
|
|
222
|
+
return self.wrap_models(self.dr_man.add_data_records(dt, num), wrapper_type)
|
|
223
|
+
|
|
224
|
+
def create_models_with_data(self, wrapper_type: type[WrappedType] | str, fields: list[FieldIdentifierMap]) \
|
|
225
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
226
|
+
"""
|
|
227
|
+
Shorthand for creating new records via the data record manager with field data to initialize the records with
|
|
228
|
+
and then returning them as wrapped record models. Useful in cases where your record model needs to have a valid
|
|
229
|
+
record ID.
|
|
230
|
+
|
|
231
|
+
Makes a webservice call to create the data records.
|
|
232
|
+
|
|
233
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
234
|
+
:param fields: The field map list to initialize the new data records with.
|
|
235
|
+
:return: The newly created record models. If a data type name was used instead of a model wrapper, then the
|
|
236
|
+
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
237
|
+
"""
|
|
238
|
+
dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
239
|
+
if isinstance(wrapper_type, str):
|
|
240
|
+
wrapper_type = None
|
|
241
|
+
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
242
|
+
return self.wrap_models(self.dr_man.add_data_records_with_data(dt, fields), wrapper_type)
|
|
243
|
+
|
|
244
|
+
def find_or_create_model(self, wrapper_type: type[WrappedType] | str, primary_identifier: FieldIdentifier,
|
|
245
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
|
|
246
|
+
-> WrappedType | PyRecordModel:
|
|
247
|
+
"""
|
|
248
|
+
Find a unique record that matches the given field values. If no such records exist, create one with the
|
|
249
|
+
identifying fields set to the desired values. If more than one record with the identifying values exists,
|
|
250
|
+
throws an exception.
|
|
251
|
+
|
|
252
|
+
The record is searched for using the primary identifier field name and value. If multiple records are returned
|
|
253
|
+
by the query on this primary identifier, then the secondary identifiers are used to filter the results.
|
|
254
|
+
|
|
255
|
+
Makes a webservice call to query for the existing record. Makes an additional webservice call if the record
|
|
256
|
+
needs to be created.
|
|
257
|
+
|
|
258
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the record.
|
|
259
|
+
:param primary_identifier: The data field name of the field to search on.
|
|
260
|
+
:param id_value: The value of the identifying field to search for.
|
|
261
|
+
:param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
|
|
262
|
+
the primary identifier.
|
|
263
|
+
:return: The record model with the identifying field value, either pulled from the system or newly created.
|
|
264
|
+
If a data type name was used instead of a model wrapper, then the returned record will be a PyRecordModel
|
|
265
|
+
instead of a WrappedRecordModel.
|
|
266
|
+
"""
|
|
267
|
+
# PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
|
|
268
|
+
# If no secondary identifiers were provided, use an empty dictionary.
|
|
269
|
+
if secondary_identifiers is None:
|
|
270
|
+
secondary_identifiers = {}
|
|
271
|
+
|
|
272
|
+
primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
|
|
273
|
+
secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
|
|
274
|
+
unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
|
|
275
|
+
secondary_identifiers)
|
|
276
|
+
# If a unique record matched the identifiers, return it.
|
|
277
|
+
if unique_record is not None:
|
|
278
|
+
return unique_record
|
|
279
|
+
|
|
280
|
+
# If none of the results matched the identifiers, create a new record with all identifiers set.
|
|
281
|
+
# Put the primary identifier and value into the secondary identifiers list and use that as the fields map
|
|
282
|
+
# for this new record.
|
|
283
|
+
secondary_identifiers.update({primary_identifier: id_value})
|
|
284
|
+
return self.create_models_with_data(wrapper_type, [secondary_identifiers])[0]
|
|
285
|
+
|
|
106
286
|
# CR-47491: Support providing a data type name string to receive PyRecordModels instead of requiring a WrapperType.
|
|
287
|
+
# CR-47523: Support a singular field value being provided for the value_list parameter.
|
|
107
288
|
def query_models(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
|
|
108
|
-
value_list: Iterable[FieldValue]
|
|
289
|
+
value_list: Iterable[FieldValue] | FieldValue,
|
|
290
|
+
page_limit: int | None = None, page_size: int | None = None) \
|
|
109
291
|
-> list[WrappedType] | list[PyRecordModel]:
|
|
110
292
|
"""
|
|
111
293
|
Shorthand for using the data record manager to query for a list of data records by field value
|
|
@@ -113,7 +295,9 @@ class RecordHandler:
|
|
|
113
295
|
|
|
114
296
|
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
115
297
|
:param field: The field to query on.
|
|
116
|
-
:param value_list: The values of the field to query on
|
|
298
|
+
:param value_list: The values of the field to query on, or a singular field value that will be automatically
|
|
299
|
+
converted to a singleton list. Note that field values of None are not supported by this method and will be
|
|
300
|
+
ignored. If you need to query for records with a null field value, use a custom report.
|
|
117
301
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
118
302
|
only functions if you set a page size or the platform enforces a page size.
|
|
119
303
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
@@ -126,8 +310,10 @@ class RecordHandler:
|
|
|
126
310
|
return self.query_models_with_criteria(wrapper_type, field, value_list, criteria, page_limit)[0]
|
|
127
311
|
|
|
128
312
|
def query_and_map_models(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
|
|
129
|
-
value_list: Iterable[FieldValue]
|
|
130
|
-
|
|
313
|
+
value_list: Iterable[FieldValue] | FieldValue,
|
|
314
|
+
page_limit: int | None = None, page_size: int | None = None,
|
|
315
|
+
*,
|
|
316
|
+
mapping_field: FieldIdentifier | None = None) \
|
|
131
317
|
-> dict[FieldValue, list[WrappedType] | list[PyRecordModel]]:
|
|
132
318
|
"""
|
|
133
319
|
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
@@ -135,7 +321,9 @@ class RecordHandler:
|
|
|
135
321
|
|
|
136
322
|
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
137
323
|
:param field: The field to query and map on.
|
|
138
|
-
:param value_list: The values of the field to query on
|
|
324
|
+
:param value_list: The values of the field to query on, or a singular field value that will be automatically
|
|
325
|
+
converted to a singleton list. Note that field values of None are not supported by this method and will be
|
|
326
|
+
ignored. If you need to query for records with a null field value, use a custom report.
|
|
139
327
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
140
328
|
only functions if you set a page size or the platform enforces a page size.
|
|
141
329
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
@@ -150,8 +338,10 @@ class RecordHandler:
|
|
|
150
338
|
mapping_field)
|
|
151
339
|
|
|
152
340
|
def query_and_unique_map_models(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
|
|
153
|
-
value_list: Iterable[FieldValue]
|
|
154
|
-
|
|
341
|
+
value_list: Iterable[FieldValue] | FieldValue,
|
|
342
|
+
page_limit: int | None = None, page_size: int | None = None,
|
|
343
|
+
*,
|
|
344
|
+
mapping_field: FieldIdentifier | None = None) \
|
|
155
345
|
-> dict[FieldValue, WrappedType | PyRecordModel]:
|
|
156
346
|
"""
|
|
157
347
|
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
@@ -160,7 +350,9 @@ class RecordHandler:
|
|
|
160
350
|
|
|
161
351
|
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
162
352
|
:param field: The field to query and map on.
|
|
163
|
-
:param value_list: The values of the field to query on
|
|
353
|
+
:param value_list: The values of the field to query on, or a singular field value that will be automatically
|
|
354
|
+
converted to a singleton list. Note that field values of None are not supported by this method and will be
|
|
355
|
+
ignored. If you need to query for records with a null field value, use a custom report.
|
|
164
356
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
165
357
|
only functions if you set a page size or the platform enforces a page size.
|
|
166
358
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
@@ -175,7 +367,7 @@ class RecordHandler:
|
|
|
175
367
|
mapping_field)
|
|
176
368
|
|
|
177
369
|
def query_models_with_criteria(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
|
|
178
|
-
value_list: Iterable[FieldValue],
|
|
370
|
+
value_list: Iterable[FieldValue] | FieldValue,
|
|
179
371
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
180
372
|
page_limit: int | None = None) \
|
|
181
373
|
-> tuple[list[WrappedType] | list[PyRecordModel], DataRecordPojoPageCriteria]:
|
|
@@ -185,7 +377,9 @@ class RecordHandler:
|
|
|
185
377
|
|
|
186
378
|
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
187
379
|
:param field: The field to query on.
|
|
188
|
-
:param value_list: The values of the field to query on
|
|
380
|
+
:param value_list: The values of the field to query on, or a singular field value that will be automatically
|
|
381
|
+
converted to a singleton list. Note that field values of None are not supported by this method and will be
|
|
382
|
+
ignored. If you need to query for records with a null field value, use a custom report.
|
|
189
383
|
:param paging_criteria: The paging criteria to start the query with.
|
|
190
384
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
191
385
|
possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
|
|
@@ -197,6 +391,8 @@ class RecordHandler:
|
|
|
197
391
|
if isinstance(wrapper_type, str):
|
|
198
392
|
wrapper_type = None
|
|
199
393
|
field: str = AliasUtil.to_data_field_name(field)
|
|
394
|
+
if isinstance(value_list, FieldValue):
|
|
395
|
+
value_list: list[FieldValue] = [value_list]
|
|
200
396
|
pager = QueryDataRecordsAutoPager(dt, field, list(value_list), self.user, paging_criteria)
|
|
201
397
|
pager.max_page = page_limit
|
|
202
398
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
@@ -336,9 +532,11 @@ class RecordHandler:
|
|
|
336
532
|
"""
|
|
337
533
|
warnings.warn("Deprecated in favor of the [System/Custom/Quick]ReportRecordAutoPager classes.", DeprecationWarning)
|
|
338
534
|
if isinstance(report_name, str):
|
|
535
|
+
# noinspection PyDeprecation
|
|
339
536
|
results: list[dict[str, FieldValue]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
|
|
340
537
|
page_limit, page_size, page_number)
|
|
341
538
|
elif isinstance(report_name, RawReportTerm):
|
|
539
|
+
# noinspection PyDeprecation
|
|
342
540
|
results: list[dict[str, FieldValue]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
|
|
343
541
|
page_limit, page_size, page_number)
|
|
344
542
|
elif isinstance(report_name, CustomReportCriteria):
|
|
@@ -351,224 +549,480 @@ class RecordHandler:
|
|
|
351
549
|
# Enforce that the given custom report has a record ID column.
|
|
352
550
|
if not any([x.data_type_name == dt and x.data_field_name == "RecordId" for x in report_name.column_list]):
|
|
353
551
|
report_name.column_list.append(ReportColumn(dt, "RecordId", FieldType.LONG))
|
|
552
|
+
# noinspection PyDeprecation
|
|
354
553
|
results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, report_name, filters,
|
|
355
554
|
page_limit, page_size, page_number)
|
|
356
555
|
else:
|
|
357
556
|
raise SapioException("Unrecognized report object.")
|
|
358
557
|
|
|
359
558
|
# Using the bracket accessor because we want to throw an exception if RecordId doesn't exist in the report.
|
|
360
|
-
# This should only possibly be the case with system reports, as quick reports will include the record ID and
|
|
559
|
+
# This should only possibly be the case with system reports, as quick reports will include the record ID, and
|
|
361
560
|
# we forced any given custom report to have a record ID column.
|
|
362
561
|
ids: list[int] = [row["RecordId"] for row in results]
|
|
363
562
|
return self.query_models_by_id(wrapper_type, ids)
|
|
364
563
|
|
|
365
|
-
|
|
564
|
+
@staticmethod
|
|
565
|
+
def map_by_id(models: Iterable[IsSapioRecord]) -> dict[int, IsSapioRecord]:
|
|
366
566
|
"""
|
|
367
|
-
|
|
567
|
+
Map the given records their record IDs.
|
|
368
568
|
|
|
369
|
-
:param
|
|
370
|
-
:return:
|
|
371
|
-
returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
569
|
+
:param models: The records to map.
|
|
570
|
+
:return: A dict mapping the record ID to each record.
|
|
372
571
|
"""
|
|
373
|
-
|
|
572
|
+
ret_dict: dict[int, SapioRecord] = {}
|
|
573
|
+
for model in models:
|
|
574
|
+
ret_dict.update({AliasUtil.to_record_id(model): model})
|
|
575
|
+
return ret_dict
|
|
374
576
|
|
|
375
|
-
|
|
577
|
+
@staticmethod
|
|
578
|
+
def map_by_field(models: Iterable[IsSapioRecord], field_name: FieldIdentifier) \
|
|
579
|
+
-> dict[FieldValue, list[IsSapioRecord]]:
|
|
376
580
|
"""
|
|
377
|
-
|
|
581
|
+
Map the given records by one of their fields. If any two records share the same field value, they'll appear in
|
|
582
|
+
the same value list.
|
|
378
583
|
|
|
379
|
-
:param
|
|
380
|
-
:param
|
|
381
|
-
:return:
|
|
382
|
-
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
584
|
+
:param models: The records to map.
|
|
585
|
+
:param field_name: The field name to map against.
|
|
586
|
+
:return: A dict mapping field values to the records with that value.
|
|
383
587
|
"""
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
588
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
589
|
+
ret_dict: dict[FieldValue, list[SapioRecord]] = {}
|
|
590
|
+
for model in models:
|
|
591
|
+
val: FieldValue = model.get_field_value(field_name)
|
|
592
|
+
ret_dict.setdefault(val, []).append(model)
|
|
593
|
+
return ret_dict
|
|
387
594
|
|
|
388
|
-
|
|
389
|
-
|
|
595
|
+
@staticmethod
|
|
596
|
+
def map_by_unique_field(models: Iterable[IsSapioRecord], field_name: FieldIdentifier) \
|
|
597
|
+
-> dict[FieldValue, IsSapioRecord]:
|
|
390
598
|
"""
|
|
391
|
-
|
|
392
|
-
|
|
599
|
+
Uniquely map the given records by one of their fields. If any two records share the same field value, throws
|
|
600
|
+
an exception.
|
|
393
601
|
|
|
394
|
-
:param
|
|
395
|
-
:param
|
|
396
|
-
:return:
|
|
397
|
-
the fields in the fields list. If a data type name was used instead of a model wrapper, then the returned
|
|
398
|
-
records will be PyRecordModels instead of WrappedRecordModels.
|
|
602
|
+
:param models: The records to map.
|
|
603
|
+
:param field_name: The field name to map against.
|
|
604
|
+
:return: A dict mapping field values to the record with that value.
|
|
399
605
|
"""
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
for model
|
|
403
|
-
model.
|
|
404
|
-
|
|
606
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
607
|
+
ret_dict: dict[FieldValue, SapioRecord] = {}
|
|
608
|
+
for model in models:
|
|
609
|
+
val: FieldValue = model.get_field_value(field_name)
|
|
610
|
+
if val in ret_dict:
|
|
611
|
+
raise SapioException(f"Value {val} encountered more than once in models list.")
|
|
612
|
+
ret_dict.update({val: model})
|
|
613
|
+
return ret_dict
|
|
405
614
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
-> WrappedType | PyRecordModel:
|
|
615
|
+
# FR-47525: Add functions for getting and setting record image bytes.
|
|
616
|
+
def get_record_image(self, record: SapioRecord) -> bytes:
|
|
409
617
|
"""
|
|
410
|
-
|
|
411
|
-
cache with the identifying fields set to the desired values. This record will be created in the system when
|
|
412
|
-
you store and commit changes. If more than one record with the identifying values exists, throws an exception.
|
|
618
|
+
Retrieve the record image for a given record.
|
|
413
619
|
|
|
414
|
-
The record
|
|
415
|
-
|
|
620
|
+
:param record: The record model to retrieve the image of.
|
|
621
|
+
:return: The file bytes of the given record's image.
|
|
622
|
+
"""
|
|
623
|
+
record: DataRecord = AliasUtil.to_data_record(record)
|
|
624
|
+
with io.BytesIO() as data_sink:
|
|
625
|
+
def consume_data(chunk: bytes):
|
|
626
|
+
data_sink.write(chunk)
|
|
416
627
|
|
|
417
|
-
|
|
628
|
+
self.dr_man.get_record_image(record, consume_data)
|
|
629
|
+
data_sink.flush()
|
|
630
|
+
data_sink.seek(0)
|
|
631
|
+
file_bytes = data_sink.read()
|
|
632
|
+
return file_bytes
|
|
418
633
|
|
|
419
|
-
|
|
420
|
-
:param primary_identifier: The data field name of the field to search on.
|
|
421
|
-
:param id_value: The value of the identifying field to search for.
|
|
422
|
-
:param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
|
|
423
|
-
the primary identifier.
|
|
424
|
-
:return: The record model with the identifying field value, either pulled from the system or newly created.
|
|
425
|
-
If a data type name was used instead of a model wrapper, then the returned record will be a PyRecordModel
|
|
426
|
-
instead of a WrappedRecordModel.
|
|
634
|
+
def set_record_image(self, record: SapioRecord, file_data: str | bytes) -> None:
|
|
427
635
|
"""
|
|
428
|
-
|
|
429
|
-
# If no secondary identifiers were provided, use an empty dictionary.
|
|
430
|
-
if secondary_identifiers is None:
|
|
431
|
-
secondary_identifiers = {}
|
|
636
|
+
Set the record image for a given record.
|
|
432
637
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
return unique_record
|
|
638
|
+
:param record: The record model to set the image of.
|
|
639
|
+
:param file_data: The file data of the image to set on the record.
|
|
640
|
+
"""
|
|
641
|
+
record: DataRecord = AliasUtil.to_data_record(record)
|
|
642
|
+
with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as stream:
|
|
643
|
+
self.dr_man.set_record_image(record, stream)
|
|
440
644
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
645
|
+
@staticmethod
|
|
646
|
+
def sum_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
647
|
+
"""
|
|
648
|
+
Sum up the numeric value of a given field across all input models. Excepts that all given models have a value.
|
|
649
|
+
If the field is an integer field, the value will be converted to a float.
|
|
446
650
|
|
|
447
|
-
|
|
651
|
+
:param models: The models to calculate the sum of.
|
|
652
|
+
:param field_name: The name of the numeric field to sum.
|
|
653
|
+
:return: The sum of the field values for the collection of models.
|
|
448
654
|
"""
|
|
449
|
-
|
|
450
|
-
|
|
655
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
656
|
+
field_sum: float = 0
|
|
657
|
+
for model in models:
|
|
658
|
+
val = model.get_field_value(field_name)
|
|
659
|
+
if isinstance(val, (int, float)):
|
|
660
|
+
field_sum += float(model.get_field_value(field_name))
|
|
661
|
+
return field_sum
|
|
451
662
|
|
|
452
|
-
|
|
663
|
+
@staticmethod
|
|
664
|
+
def mean_of_field(models: Collection[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
665
|
+
"""
|
|
666
|
+
Calculate the average (arithmetic mean) of the numeric value of a given field across all input models. Excepts
|
|
667
|
+
that all given models have a value. If the field is an integer field, the value will be converted to a float.
|
|
453
668
|
|
|
454
|
-
:param
|
|
455
|
-
:param
|
|
456
|
-
:return: The
|
|
457
|
-
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
669
|
+
:param models: The models to calculate the mean of.
|
|
670
|
+
:param field_name: The name of the numeric field to mean.
|
|
671
|
+
:return: The mean of the field values for the collection of models.
|
|
458
672
|
"""
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
673
|
+
return RecordHandler.sum_of_field(models, field_name) / len(models)
|
|
674
|
+
|
|
675
|
+
@staticmethod
|
|
676
|
+
def get_newest_record(records: Iterable[IsSapioRecord]) -> IsSapioRecord:
|
|
677
|
+
"""
|
|
678
|
+
Get the newest record from a list of records.
|
|
679
|
+
|
|
680
|
+
:param records: The list of records.
|
|
681
|
+
:return: The input record with the highest record ID. None if the input list is empty.
|
|
682
|
+
"""
|
|
683
|
+
return max(records, key=lambda x: x.record_id)
|
|
684
|
+
|
|
685
|
+
# FR-46696: Add a function for getting the oldest record in a list, just like we have one for the newest record.
|
|
686
|
+
@staticmethod
|
|
687
|
+
def get_oldest_record(records: Iterable[IsSapioRecord]) -> IsSapioRecord:
|
|
688
|
+
"""
|
|
689
|
+
Get the oldest record from a list of records.
|
|
690
|
+
|
|
691
|
+
:param records: The list of records.
|
|
692
|
+
:return: The input record with the lowest record ID. None if the input list is empty.
|
|
693
|
+
"""
|
|
694
|
+
return min(records, key=lambda x: x.record_id)
|
|
695
|
+
|
|
696
|
+
@staticmethod
|
|
697
|
+
def get_min_record(records: list[IsSapioRecord], field: FieldIdentifier) -> IsSapioRecord:
|
|
698
|
+
"""
|
|
699
|
+
Get the record model with the minimum value of a given field from a list of record models.
|
|
700
|
+
|
|
701
|
+
:param records: The list of record models to search through.
|
|
702
|
+
:param field: The field to find the minimum value of.
|
|
703
|
+
:return: The record model with the minimum value of the given field.
|
|
704
|
+
"""
|
|
705
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
706
|
+
return min(records, key=lambda x: x.get_field_value(field))
|
|
707
|
+
|
|
708
|
+
@staticmethod
|
|
709
|
+
def get_max_record(records: list[IsSapioRecord], field: FieldIdentifier) -> IsSapioRecord:
|
|
710
|
+
"""
|
|
711
|
+
Get the record model with the maximum value of a given field from a list of record models.
|
|
712
|
+
|
|
713
|
+
:param records: The list of record models to search through.
|
|
714
|
+
:param field: The field to find the maximum value of.
|
|
715
|
+
:return: The record model with the maximum value of the given field.
|
|
716
|
+
"""
|
|
717
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
718
|
+
return max(records, key=lambda x: x.get_field_value(field))
|
|
719
|
+
|
|
720
|
+
# FR-47522: Add RecordHandler functions that copy from the RecordModelUtil class in our Java utilities.
|
|
721
|
+
@staticmethod
|
|
722
|
+
def get_values_list(records: list[RecordModel], field: FieldIdentifier) -> list[FieldValue]:
|
|
723
|
+
"""
|
|
724
|
+
Get a list of field values from a list of record models.
|
|
725
|
+
|
|
726
|
+
:param records: The list of record models to get the field values from.
|
|
727
|
+
:param field: The field to get the values of.
|
|
728
|
+
:return: A list of field values from the input record models. The values are in the same order as the input
|
|
729
|
+
record models.
|
|
730
|
+
"""
|
|
731
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
732
|
+
return [x.get_field_value(field) for x in records]
|
|
733
|
+
|
|
734
|
+
@staticmethod
|
|
735
|
+
def get_values_set(records: list[RecordModel], field: FieldIdentifier) -> set[FieldValue]:
|
|
736
|
+
"""
|
|
737
|
+
Get a set of field values from a list of record models.
|
|
738
|
+
|
|
739
|
+
:param records: The list of record models to get the field values from.
|
|
740
|
+
:param field: The field to get the values of.
|
|
741
|
+
:return: A set of field values from the input record models.
|
|
742
|
+
"""
|
|
743
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
744
|
+
return {x.get_field_value(field) for x in records}
|
|
745
|
+
|
|
746
|
+
@staticmethod
|
|
747
|
+
def set_values(records: list[RecordModel], field: FieldIdentifier, value: FieldValue) -> None:
|
|
748
|
+
"""
|
|
749
|
+
Set the value of a field on a list of record models.
|
|
750
|
+
|
|
751
|
+
:param records: The list of record models to set the field value on.
|
|
752
|
+
:param field: The field to set the value of.
|
|
753
|
+
:param value: The value to set the field to for all input records.
|
|
754
|
+
"""
|
|
755
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
756
|
+
for record in records:
|
|
757
|
+
record.set_field_value(field, value)
|
|
758
|
+
|
|
759
|
+
@staticmethod
|
|
760
|
+
def values_to_field_maps(field_name: FieldIdentifier, values: Iterable[FieldValue],
|
|
761
|
+
existing_fields: list[FieldMap] | None = None) -> list[FieldMap]:
|
|
762
|
+
"""
|
|
763
|
+
Add a list of values for a specific field to a list of dictionaries pairing each value to that field name.
|
|
764
|
+
|
|
765
|
+
:param field_name: The name of the field that the values are from.
|
|
766
|
+
:param values: A list of field values.
|
|
767
|
+
:param existing_fields: An optional existing fields map list to add the new values to. Values are added in the
|
|
768
|
+
list in the same order that they appear. If no existing fields are provided, returns a new fields map list.
|
|
769
|
+
:return: A fields map list that contains the given values mapped by the given field name.
|
|
770
|
+
"""
|
|
771
|
+
# Update the existing fields map list if one is given.
|
|
772
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
773
|
+
existing_fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(existing_fields)
|
|
774
|
+
if existing_fields:
|
|
775
|
+
values = list(values)
|
|
776
|
+
# The number of new values must match the length of the existing fields list.
|
|
777
|
+
if len(values) != len(existing_fields):
|
|
778
|
+
raise SapioException(f"Length of \"{field_name}\" values does not match the existing fields length.")
|
|
779
|
+
for field, value in zip(existing_fields, values):
|
|
780
|
+
field.update({field_name: value})
|
|
781
|
+
return existing_fields
|
|
782
|
+
# Otherwise, create a new fields map list.
|
|
783
|
+
return [{field_name: value} for value in values]
|
|
463
784
|
|
|
464
|
-
|
|
465
|
-
|
|
785
|
+
@staticmethod
|
|
786
|
+
def get_from_all(records: Iterable[RecordModel], getter: _PropertyGetter[_PropertyType]) \
|
|
787
|
+
-> list[RecordModelPropertyType]:
|
|
466
788
|
"""
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
record
|
|
789
|
+
Use a getter property on all records in a list of record models. For example, you can iterate over a list of
|
|
790
|
+
record models using a getter of Ancestors.of_type(SampleModel) to get all the SampleModel ancestors from each
|
|
791
|
+
record.
|
|
470
792
|
|
|
471
|
-
|
|
793
|
+
:param records: The list of record models to get the property from.
|
|
794
|
+
:param getter: The getter to use to get the property from each record.
|
|
795
|
+
:return: A list of the property values from the input record models. The value at the matching index of the
|
|
796
|
+
input records is the results of using the getter on that record.
|
|
797
|
+
"""
|
|
798
|
+
return [x.get(getter) for x in records]
|
|
472
799
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
800
|
+
@staticmethod
|
|
801
|
+
def set_on_all(records: Iterable[RecordModel], setter: _PropertySetter[_PropertyType]) \
|
|
802
|
+
-> list[RecordModelPropertyType]:
|
|
477
803
|
"""
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
482
|
-
return self.wrap_models(self.dr_man.add_data_records_with_data(dt, fields), wrapper_type)
|
|
804
|
+
Use a setter property on all records in a list of record models. For example, you can iterate over a list of
|
|
805
|
+
record models user a setter of ForwardSideLink.ref(field_name, record) to set a forward side link on each
|
|
806
|
+
record.
|
|
483
807
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
808
|
+
:param records: The list of record models to set the property on.
|
|
809
|
+
:param setter: The setter to use to set the property on each record.
|
|
810
|
+
:return: A list of the property values that were set on the input record models. The value at the matching index
|
|
811
|
+
of the input records is the results of using the setter on that record.
|
|
487
812
|
"""
|
|
488
|
-
|
|
489
|
-
identifying fields set to the desired values. If more than one record with the identifying values exists,
|
|
490
|
-
throws an exception.
|
|
813
|
+
return [x.set(setter) for x in records]
|
|
491
814
|
|
|
492
|
-
|
|
493
|
-
|
|
815
|
+
@staticmethod
|
|
816
|
+
def add_to_all(records: Iterable[RecordModel], adder: _PropertyAdder[_PropertyType]) \
|
|
817
|
+
-> list[RecordModelPropertyType]:
|
|
818
|
+
"""
|
|
819
|
+
Use an adder property on all records in a list of record models. For example, you can iterate over a list of
|
|
820
|
+
record models using an adder of Child.create(SampleModel) to create a new SampleModel child on each record.
|
|
494
821
|
|
|
495
|
-
|
|
496
|
-
|
|
822
|
+
:param records: The list of record models to add the property to.
|
|
823
|
+
:param adder: The adder to use to add the property to each record.
|
|
824
|
+
:return: A list of the property values that were added to the input record models. The value at the matching
|
|
825
|
+
index of the input records is the results of using the adder on that record.
|
|
826
|
+
"""
|
|
827
|
+
return [x.add(adder) for x in records]
|
|
497
828
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
829
|
+
@staticmethod
|
|
830
|
+
def remove_from_all(records: Iterable[RecordModel], remover: _PropertyRemover[_PropertyType]) \
|
|
831
|
+
-> list[RecordModelPropertyType]:
|
|
832
|
+
"""
|
|
833
|
+
Use a remover property on all records in a list of record models. For example, you can iterate over a list of
|
|
834
|
+
record models using a remover of Parents.ref(records) to remove a list of parents from each record.
|
|
835
|
+
|
|
836
|
+
:param records: The list of record models to remove the property from.
|
|
837
|
+
:param remover: The remover to use to remove the property from each record.
|
|
838
|
+
:return: A list of the property values that were removed from the input record models. The value at the matching
|
|
839
|
+
index of the input records is the results of using the remover on that record.
|
|
840
|
+
"""
|
|
841
|
+
return [x.remove(remover) for x in records]
|
|
842
|
+
|
|
843
|
+
# FR-47527: Created functions for manipulating relationships between records,
|
|
844
|
+
def get_extension(self, model: RecordModel, wrapper_type: type[WrappedType] | str) \
|
|
845
|
+
-> WrappedType | PyRecordModel | None:
|
|
846
|
+
"""
|
|
847
|
+
Given a record with an extension record related to it, return the extension record as a record model.
|
|
848
|
+
This will retrieve an extension record without doing a webservice request to the server. The input record and
|
|
849
|
+
extension record will be considered related to one another if you later use load_child or load_parent on the
|
|
850
|
+
input record or extension record respectively.
|
|
851
|
+
|
|
852
|
+
:param model: The record model to get the extension for.
|
|
853
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the extension record. If a data
|
|
854
|
+
type name is provided, the returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
855
|
+
:return: The extension record model for the input record model, or None if no extension record exists.
|
|
856
|
+
"""
|
|
857
|
+
ext_dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
858
|
+
ext_fields: FieldMap = {}
|
|
859
|
+
for field, value in AliasUtil.to_field_map(model).items():
|
|
860
|
+
if field.startswith(ext_dt + "."):
|
|
861
|
+
ext_fields[field.removeprefix(ext_dt + ".")] = value
|
|
862
|
+
if not ext_fields or ext_fields.get("RecordId") is None:
|
|
863
|
+
return None
|
|
864
|
+
ext_rec: DataRecord = DataRecord(ext_dt, ext_fields.get("RecordId"), ext_fields)
|
|
865
|
+
ext_model: WrappedType | PyRecordModel = self.wrap_model(ext_rec, wrapper_type)
|
|
866
|
+
self._spoof_child_load(model, ext_model)
|
|
867
|
+
self._spoof_parent_load(ext_model, model)
|
|
868
|
+
return ext_model
|
|
869
|
+
|
|
870
|
+
def get_or_add_parent(self, record: RecordModel, parent_type: type[WrappedType] | str) \
|
|
871
|
+
-> WrappedType | PyRecordModel:
|
|
506
872
|
"""
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
873
|
+
Given a record model, retrieve the singular parent record model of a given type. If a parent of the given type
|
|
874
|
+
does not exist, a new one will be created. The parents of the given data type must already be loaded.
|
|
875
|
+
|
|
876
|
+
:param record: The record model to get the parent of.
|
|
877
|
+
:param parent_type: The record model wrapper of the parent, or the data type name of the parent. If a data type
|
|
878
|
+
name is provided, the returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
879
|
+
:return: The parent record model of the given type.
|
|
880
|
+
"""
|
|
881
|
+
parent_dt: str = AliasUtil.to_data_type_name(parent_type)
|
|
882
|
+
wrapper: type[WrappedType] | None = parent_type if isinstance(parent_type, type) else None
|
|
883
|
+
record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
|
|
884
|
+
parent: PyRecordModel | None = record.get(Parent.of_type_name(parent_dt))
|
|
885
|
+
if parent is not None:
|
|
886
|
+
return self.wrap_model(parent, wrapper) if wrapper else parent
|
|
887
|
+
return record.add(Parent.create(wrapper)) if wrapper else record.add(Parent.create_by_name(parent_dt))
|
|
888
|
+
|
|
889
|
+
def get_or_add_child(self, record: RecordModel, child_type: type[WrappedType] | str) -> WrappedType | PyRecordModel:
|
|
890
|
+
"""
|
|
891
|
+
Given a record model, retrieve the singular child record model of a given type. If a child of the given type
|
|
892
|
+
does not exist, a new one will be created. The children of the given data type must already be loaded.
|
|
893
|
+
|
|
894
|
+
:param record: The record model to get the child of.
|
|
895
|
+
:param child_type: The record model wrapper of the child, or the data type name of the child. If a data type
|
|
896
|
+
name is provided, the returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
897
|
+
:return: The child record model of the given type.
|
|
898
|
+
"""
|
|
899
|
+
child_dt: str = AliasUtil.to_data_type_name(child_type)
|
|
900
|
+
wrapper: type[WrappedType] | None = child_type if isinstance(child_type, type) else None
|
|
901
|
+
record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
|
|
902
|
+
child: PyRecordModel | None = record.get(Child.of_type_name(child_dt))
|
|
903
|
+
if child is not None:
|
|
904
|
+
return self.wrap_model(child, wrapper) if wrapper else child
|
|
905
|
+
return record.add(Child.create(wrapper)) if wrapper else record.add(Child.create_by_name(child_dt))
|
|
906
|
+
|
|
907
|
+
def get_or_add_side_link(self, record: RecordModel, side_link_field: FieldIdentifier,
|
|
908
|
+
side_link_type: type[WrappedType] | str) -> WrappedType | PyRecordModel:
|
|
909
|
+
"""
|
|
910
|
+
Given a record model, retrieve the singular side link record model of a given type. If a side link of the given
|
|
911
|
+
type does not exist, a new one will be created. The side links of the given data type must already be loaded.
|
|
912
|
+
|
|
913
|
+
:param record: The record model to get the side link of.
|
|
914
|
+
:param side_link_field: The field name of the side link to get.
|
|
915
|
+
:param side_link_type: The record model wrapper of the side link, or the data type name of the side link. If a
|
|
916
|
+
data type name is provided, the returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
917
|
+
:return: The side link record model of the given type.
|
|
918
|
+
"""
|
|
919
|
+
side_link_field: str = AliasUtil.to_data_field_name(side_link_field)
|
|
920
|
+
wrapper: type[WrappedType] | None = side_link_type if isinstance(side_link_type, type) else None
|
|
921
|
+
record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
|
|
922
|
+
side_link: PyRecordModel | None = record.get(ForwardSideLink.of(side_link_field))
|
|
923
|
+
if side_link is not None:
|
|
924
|
+
return self.wrap_model(side_link, wrapper) if wrapper else side_link
|
|
925
|
+
side_link: WrappedType | PyRecordModel = self.add_model(side_link_type)
|
|
926
|
+
record.set(ForwardSideLink.ref(side_link_field, side_link))
|
|
927
|
+
return side_link
|
|
511
928
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
return unique_record
|
|
929
|
+
@staticmethod
|
|
930
|
+
def set_parents(record: RecordModel, parents: Iterable[RecordModel], parent_type: DataTypeIdentifier) -> None:
|
|
931
|
+
"""
|
|
932
|
+
Set the parents of a record model to a list of parent record models of a given type. The parents of the given
|
|
933
|
+
data type must already be loaded. This method will add the parents to the record model if they are not already
|
|
934
|
+
parents, and remove any existing parents that are not in the input list.
|
|
519
935
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
936
|
+
:param record: The record model to set the parents of.
|
|
937
|
+
:param parents: The list of parent record models to set as the parents of the input record model.
|
|
938
|
+
:param parent_type: The data type identifier of the parent record models.
|
|
939
|
+
"""
|
|
940
|
+
parent_dt: str = AliasUtil.to_data_type_name(parent_type)
|
|
941
|
+
existing_parents: list[PyRecordModel] = record.get(Parents.of_type_name(parent_dt))
|
|
942
|
+
for parent in parents:
|
|
943
|
+
if parent not in existing_parents:
|
|
944
|
+
record.add(Parent.ref(parent))
|
|
945
|
+
for parent in existing_parents:
|
|
946
|
+
if parent not in parents:
|
|
947
|
+
record.remove(Parent.ref(parent))
|
|
525
948
|
|
|
526
949
|
@staticmethod
|
|
527
|
-
def
|
|
528
|
-
|
|
950
|
+
def set_children(record: RecordModel, children: Iterable[RecordModel], child_type: DataTypeIdentifier) -> None:
|
|
951
|
+
"""
|
|
952
|
+
Set the children of a record model to a list of child record models of a given type. The children of the given
|
|
953
|
+
data type must already be loaded. This method will add the children to the record model if they are not already
|
|
954
|
+
children, and remove any existing children that are not in the input list.
|
|
955
|
+
|
|
956
|
+
:param record: The record model to set the children of.
|
|
957
|
+
:param children: The list of child record models to set as the children of the input record model.
|
|
958
|
+
:param child_type: The data type identifier of the child record models.
|
|
959
|
+
"""
|
|
960
|
+
child_dt: str = AliasUtil.to_data_type_name(child_type)
|
|
961
|
+
existing_children: list[PyRecordModel] = record.get(Children.of_type_name(child_dt))
|
|
962
|
+
for child in children:
|
|
963
|
+
if child not in existing_children:
|
|
964
|
+
record.add(Child.ref(child))
|
|
965
|
+
for child in existing_children:
|
|
966
|
+
if child not in children:
|
|
967
|
+
record.remove(Child.ref(child))
|
|
968
|
+
|
|
969
|
+
# CR-47717: Update the map_[to/by]_[relationship] functions to allow PyRecordModels to be provided and returned
|
|
970
|
+
# instead of only using WrappedRecordModels and wrapper types.
|
|
971
|
+
@staticmethod
|
|
972
|
+
def map_to_parent(models: Iterable[IsRecordModel], parent_type: type[WrappedType] | str) \
|
|
973
|
+
-> dict[IsRecordModel, WrappedType | PyRecordModel]:
|
|
529
974
|
"""
|
|
530
975
|
Map a list of record models to a single parent of a given type. The parents must already be loaded.
|
|
531
976
|
|
|
532
977
|
:param models: A list of record models.
|
|
533
|
-
:param parent_type: The record model wrapper of the
|
|
978
|
+
:param parent_type: The record model wrapper or data type name of the parents. If a data type name is
|
|
979
|
+
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
534
980
|
:return: A dict[ModelType, ParentType]. If an input model doesn't have a parent of the given parent type, then
|
|
535
981
|
it will map to None.
|
|
536
982
|
"""
|
|
537
|
-
return_dict: dict[
|
|
983
|
+
return_dict: dict[RecordModel, WrappedType | PyRecordModel] = {}
|
|
538
984
|
for model in models:
|
|
539
|
-
|
|
985
|
+
if isinstance(parent_type, str):
|
|
986
|
+
return_dict[model] = model.get(Parent.of_type_name(parent_type))
|
|
987
|
+
else:
|
|
988
|
+
return_dict[model] = model.get(Parent.of_type(parent_type))
|
|
540
989
|
return return_dict
|
|
541
990
|
|
|
542
991
|
@staticmethod
|
|
543
|
-
def map_to_parents(models: Iterable[
|
|
544
|
-
-> dict[
|
|
992
|
+
def map_to_parents(models: Iterable[IsRecordModel], parent_type: type[WrappedType] | str) \
|
|
993
|
+
-> dict[IsRecordModel, list[WrappedType] | list[PyRecordModel]]:
|
|
545
994
|
"""
|
|
546
995
|
Map a list of record models to a list parents of a given type. The parents must already be loaded.
|
|
547
996
|
|
|
548
997
|
:param models: A list of record models.
|
|
549
|
-
:param parent_type: The record model wrapper of the parents.
|
|
998
|
+
:param parent_type: The record model wrapper or data type name of the parents. If a data type name is
|
|
999
|
+
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
550
1000
|
:return: A dict[ModelType, list[ParentType]]. If an input model doesn't have a parent of the given parent type,
|
|
551
1001
|
then it will map to an empty list.
|
|
552
1002
|
"""
|
|
553
|
-
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
1003
|
+
return_dict: dict[WrappedRecordModel, list[WrappedType] | list[PyRecordModel]] = {}
|
|
554
1004
|
for model in models:
|
|
555
|
-
|
|
1005
|
+
if isinstance(parent_type, str):
|
|
1006
|
+
return_dict[model] = model.get(Parents.of_type_name(parent_type))
|
|
1007
|
+
else:
|
|
1008
|
+
return_dict[model] = model.get(Parents.of_type(parent_type))
|
|
556
1009
|
return return_dict
|
|
557
1010
|
|
|
558
1011
|
@staticmethod
|
|
559
|
-
def map_by_parent(models: Iterable[
|
|
560
|
-
-> dict[WrappedType,
|
|
1012
|
+
def map_by_parent(models: Iterable[IsRecordModel], parent_type: type[WrappedType] | str) \
|
|
1013
|
+
-> dict[WrappedType | PyRecordModel, IsRecordModel]:
|
|
561
1014
|
"""
|
|
562
1015
|
Take a list of record models and map them by their parent. Essentially an inversion of map_to_parent.
|
|
563
1016
|
If two records share the same parent, an exception will be thrown. The parents must already be loaded.
|
|
564
1017
|
|
|
565
1018
|
:param models: A list of record models.
|
|
566
|
-
:param parent_type: The record model wrapper of the parents.
|
|
1019
|
+
:param parent_type: The record model wrapper or data type name of the parents. If a data type name is
|
|
1020
|
+
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
567
1021
|
:return: A dict[ParentType, ModelType]. If an input model doesn't have a parent of the given parent type,
|
|
568
1022
|
then it will not be in the resulting dictionary.
|
|
569
1023
|
"""
|
|
570
|
-
to_parent: dict[
|
|
571
|
-
by_parent: dict[WrappedType,
|
|
1024
|
+
to_parent: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler.map_to_parent(models, parent_type)
|
|
1025
|
+
by_parent: dict[WrappedType | PyRecordModel, RecordModel] = {}
|
|
572
1026
|
for record, parent in to_parent.items():
|
|
573
1027
|
if parent is None:
|
|
574
1028
|
continue
|
|
@@ -579,70 +1033,81 @@ class RecordHandler:
|
|
|
579
1033
|
return by_parent
|
|
580
1034
|
|
|
581
1035
|
@staticmethod
|
|
582
|
-
def map_by_parents(models: Iterable[
|
|
583
|
-
-> dict[WrappedType, list[
|
|
1036
|
+
def map_by_parents(models: Iterable[IsRecordModel], parent_type: type[WrappedType] | str) \
|
|
1037
|
+
-> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
|
|
584
1038
|
"""
|
|
585
1039
|
Take a list of record models and map them by their parents. Essentially an inversion of map_to_parents. Input
|
|
586
1040
|
models that share a parent will end up in the same list. The parents must already be loaded.
|
|
587
1041
|
|
|
588
1042
|
:param models: A list of record models.
|
|
589
|
-
:param parent_type: The record model wrapper of the parents.
|
|
1043
|
+
:param parent_type: The record model wrapper or data type name of the parents. If a data type name is
|
|
1044
|
+
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
590
1045
|
:return: A dict[ParentType, list[ModelType]]. If an input model doesn't have a parent of the given parent type,
|
|
591
1046
|
then it will not be in the resulting dictionary.
|
|
592
1047
|
"""
|
|
593
|
-
to_parents: dict[
|
|
594
|
-
|
|
1048
|
+
to_parents: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = RecordHandler\
|
|
1049
|
+
.map_to_parents(models, parent_type)
|
|
1050
|
+
by_parents: dict[WrappedType | PyRecordModel, list[RecordModel]] = {}
|
|
595
1051
|
for record, parents in to_parents.items():
|
|
596
1052
|
for parent in parents:
|
|
597
1053
|
by_parents.setdefault(parent, []).append(record)
|
|
598
1054
|
return by_parents
|
|
599
1055
|
|
|
600
1056
|
@staticmethod
|
|
601
|
-
def map_to_child(models: Iterable[
|
|
602
|
-
-> dict[
|
|
1057
|
+
def map_to_child(models: Iterable[IsRecordModel], child_type: type[WrappedType] | str) \
|
|
1058
|
+
-> dict[IsRecordModel, WrappedType | PyRecordModel]:
|
|
603
1059
|
"""
|
|
604
1060
|
Map a list of record models to a single child of a given type. The children must already be loaded.
|
|
605
1061
|
|
|
606
1062
|
:param models: A list of record models.
|
|
607
|
-
:param child_type: The record model wrapper of the
|
|
1063
|
+
:param child_type: The record model wrapper or data type name of the children. If a data type name is
|
|
1064
|
+
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
608
1065
|
:return: A dict[ModelType, ChildType]. If an input model doesn't have a child of the given child type, then
|
|
609
1066
|
it will map to None.
|
|
610
1067
|
"""
|
|
611
|
-
return_dict: dict[
|
|
1068
|
+
return_dict: dict[RecordModel, WrappedType | PyRecordModel] = {}
|
|
612
1069
|
for model in models:
|
|
613
|
-
|
|
1070
|
+
if isinstance(child_type, str):
|
|
1071
|
+
return_dict[model] = model.get(Child.of_type_name(child_type))
|
|
1072
|
+
else:
|
|
1073
|
+
return_dict[model] = model.get(Child.of_type(child_type))
|
|
614
1074
|
return return_dict
|
|
615
1075
|
|
|
616
1076
|
@staticmethod
|
|
617
|
-
def map_to_children(models: Iterable[
|
|
618
|
-
-> dict[
|
|
1077
|
+
def map_to_children(models: Iterable[IsRecordModel], child_type: type[WrappedType] | str) \
|
|
1078
|
+
-> dict[IsRecordModel, list[WrappedType] | PyRecordModel]:
|
|
619
1079
|
"""
|
|
620
1080
|
Map a list of record models to a list children of a given type. The children must already be loaded.
|
|
621
1081
|
|
|
622
1082
|
:param models: A list of record models.
|
|
623
|
-
:param child_type: The record model wrapper of the children.
|
|
1083
|
+
:param child_type: The record model wrapper or data type name of the children. If a data type name is
|
|
1084
|
+
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
624
1085
|
:return: A dict[ModelType, list[ChildType]]. If an input model doesn't have children of the given child type,
|
|
625
1086
|
then it will map to an empty list.
|
|
626
1087
|
"""
|
|
627
|
-
return_dict: dict[
|
|
1088
|
+
return_dict: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = {}
|
|
628
1089
|
for model in models:
|
|
629
|
-
|
|
1090
|
+
if isinstance(child_type, str):
|
|
1091
|
+
return_dict[model] = model.get(Children.of_type_name(child_type))
|
|
1092
|
+
else:
|
|
1093
|
+
return_dict[model] = model.get(Children.of_type(child_type))
|
|
630
1094
|
return return_dict
|
|
631
1095
|
|
|
632
1096
|
@staticmethod
|
|
633
|
-
def map_by_child(models: Iterable[
|
|
634
|
-
-> dict[WrappedType,
|
|
1097
|
+
def map_by_child(models: Iterable[IsRecordModel], child_type: type[WrappedType] | str) \
|
|
1098
|
+
-> dict[WrappedType | str, IsRecordModel]:
|
|
635
1099
|
"""
|
|
636
1100
|
Take a list of record models and map them by their children. Essentially an inversion of map_to_child.
|
|
637
1101
|
If two records share the same child, an exception will be thrown. The children must already be loaded.
|
|
638
1102
|
|
|
639
1103
|
:param models: A list of record models.
|
|
640
|
-
:param child_type: The record model wrapper of the children.
|
|
1104
|
+
:param child_type: The record model wrapper or data type name of the children. If a data type name is
|
|
1105
|
+
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
641
1106
|
:return: A dict[ChildType, ModelType]. If an input model doesn't have a child of the given child type,
|
|
642
1107
|
then it will not be in the resulting dictionary.
|
|
643
1108
|
"""
|
|
644
|
-
to_child: dict[
|
|
645
|
-
by_child: dict[WrappedType,
|
|
1109
|
+
to_child: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler.map_to_child(models, child_type)
|
|
1110
|
+
by_child: dict[WrappedType | PyRecordModel, RecordModel] = {}
|
|
646
1111
|
for record, child in to_child.items():
|
|
647
1112
|
if child is None:
|
|
648
1113
|
continue
|
|
@@ -653,116 +1118,105 @@ class RecordHandler:
|
|
|
653
1118
|
return by_child
|
|
654
1119
|
|
|
655
1120
|
@staticmethod
|
|
656
|
-
def map_by_children(models: Iterable[
|
|
657
|
-
-> dict[WrappedType, list[
|
|
1121
|
+
def map_by_children(models: Iterable[IsRecordModel], child_type: type[WrappedType] | str) \
|
|
1122
|
+
-> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
|
|
658
1123
|
"""
|
|
659
1124
|
Take a list of record models and map them by their children. Essentially an inversion of map_to_children. Input
|
|
660
1125
|
models that share a child will end up in the same list. The children must already be loaded.
|
|
661
1126
|
|
|
662
1127
|
:param models: A list of record models.
|
|
663
|
-
:param child_type: The record model wrapper of the children.
|
|
1128
|
+
:param child_type: The record model wrapper or data type name of the children. If a data type name is
|
|
1129
|
+
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
664
1130
|
:return: A dict[ChildType, list[ModelType]]. If an input model doesn't have children of the given child type,
|
|
665
1131
|
then it will not be in the resulting dictionary.
|
|
666
1132
|
"""
|
|
667
|
-
to_children: dict[
|
|
668
|
-
|
|
1133
|
+
to_children: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = RecordHandler\
|
|
1134
|
+
.map_to_children(models, child_type)
|
|
1135
|
+
by_children: dict[WrappedType | PyRecordModel, list[RecordModel]] = {}
|
|
669
1136
|
for record, children in to_children.items():
|
|
670
1137
|
for child in children:
|
|
671
1138
|
by_children.setdefault(child, []).append(record)
|
|
672
1139
|
return by_children
|
|
673
1140
|
|
|
674
1141
|
@staticmethod
|
|
675
|
-
def map_to_forward_side_link(models: Iterable[
|
|
676
|
-
side_link_type: type[WrappedType]
|
|
1142
|
+
def map_to_forward_side_link(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
|
|
1143
|
+
side_link_type: type[WrappedType] | None) \
|
|
1144
|
+
-> dict[IsRecordModel, WrappedType | PyRecordModel]:
|
|
677
1145
|
"""
|
|
678
1146
|
Map a list of record models to their forward side link. The forward side link must already be loaded.
|
|
679
1147
|
|
|
680
1148
|
:param models: A list of record models.
|
|
681
1149
|
:param field_name: The field name on the record models where the side link is located.
|
|
682
|
-
:param side_link_type: The record model wrapper of the forward side link.
|
|
1150
|
+
:param side_link_type: The record model wrapper of the forward side link. If None, the side links will
|
|
1151
|
+
be returned as PyRecordModels instead of WrappedRecordModels.
|
|
683
1152
|
:return: A dict[ModelType, SlideLink]. If an input model doesn't have a forward side link of the given type,
|
|
684
1153
|
then it will map to None.
|
|
685
1154
|
"""
|
|
686
1155
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
687
|
-
return_dict: dict[
|
|
1156
|
+
return_dict: dict[RecordModel, WrappedType | PyRecordModel] = {}
|
|
688
1157
|
for model in models:
|
|
689
|
-
return_dict[model] = model.
|
|
1158
|
+
return_dict[model] = model.get(ForwardSideLink.of(field_name, side_link_type))
|
|
690
1159
|
return return_dict
|
|
691
1160
|
|
|
692
1161
|
@staticmethod
|
|
693
|
-
def
|
|
694
|
-
|
|
1162
|
+
def map_by_forward_side_link(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
|
|
1163
|
+
side_link_type: type[WrappedType] | None) \
|
|
1164
|
+
-> dict[WrappedType | PyRecordModel, IsRecordModel]:
|
|
695
1165
|
"""
|
|
696
1166
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
697
|
-
map_to_forward_side_link
|
|
1167
|
+
map_to_forward_side_link, but if two records share the same forward link, an exception is thrown.
|
|
698
1168
|
The forward side link must already be loaded.
|
|
699
1169
|
|
|
700
1170
|
:param models: A list of record models.
|
|
701
1171
|
:param field_name: The field name on the record models where the side link is located.
|
|
702
|
-
:param side_link_type: The record model wrapper of the forward side links.
|
|
703
|
-
|
|
1172
|
+
:param side_link_type: The record model wrapper of the forward side links. If None, the side links will
|
|
1173
|
+
be returned as PyRecordModels instead of WrappedRecordModels.
|
|
1174
|
+
:return: A dict[SideLink, ModelType]. If an input model doesn't have a forward side link of the given type
|
|
704
1175
|
pointing to it, then it will not be in the resulting dictionary.
|
|
705
1176
|
"""
|
|
706
1177
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
707
|
-
to_side_link: dict[
|
|
1178
|
+
to_side_link: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler\
|
|
708
1179
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
709
|
-
by_side_link: dict[WrappedType,
|
|
1180
|
+
by_side_link: dict[WrappedType | PyRecordModel, RecordModel] = {}
|
|
710
1181
|
for record, side_link in to_side_link.items():
|
|
711
1182
|
if side_link is None:
|
|
712
1183
|
continue
|
|
713
|
-
|
|
1184
|
+
if side_link in by_side_link:
|
|
1185
|
+
raise SapioException(f"Side link {side_link.data_type_name} {side_link.record_id} encountered more "
|
|
1186
|
+
f"than once in models list.")
|
|
1187
|
+
by_side_link[side_link] = record
|
|
714
1188
|
return by_side_link
|
|
715
1189
|
|
|
716
1190
|
@staticmethod
|
|
717
|
-
def
|
|
718
|
-
|
|
1191
|
+
def map_by_forward_side_links(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
|
|
1192
|
+
side_link_type: type[WrappedType] | None) \
|
|
1193
|
+
-> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
|
|
719
1194
|
"""
|
|
720
1195
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
721
|
-
map_to_forward_side_link
|
|
1196
|
+
map_to_forward_side_link. Input models that share a forward side link will end up in the same list.
|
|
722
1197
|
The forward side link must already be loaded.
|
|
723
1198
|
|
|
724
1199
|
:param models: A list of record models.
|
|
725
1200
|
:param field_name: The field name on the record models where the side link is located.
|
|
726
|
-
:param side_link_type: The record model wrapper of the forward side links.
|
|
727
|
-
|
|
1201
|
+
:param side_link_type: The record model wrapper of the forward side links. If None, the side links will
|
|
1202
|
+
be returned as PyRecordModels instead of WrappedRecordModels.
|
|
1203
|
+
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
|
|
728
1204
|
pointing to it, then it will not be in the resulting dictionary.
|
|
729
1205
|
"""
|
|
730
1206
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
731
|
-
to_side_link: dict[
|
|
1207
|
+
to_side_link: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler\
|
|
732
1208
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
733
|
-
by_side_link: dict[WrappedType,
|
|
1209
|
+
by_side_link: dict[WrappedType | PyRecordModel, list[RecordModel]] = {}
|
|
734
1210
|
for record, side_link in to_side_link.items():
|
|
735
1211
|
if side_link is None:
|
|
736
1212
|
continue
|
|
737
|
-
|
|
738
|
-
raise SapioException(f"Side link {side_link.data_type_name} {side_link.record_id} encountered more "
|
|
739
|
-
f"than once in models list.")
|
|
740
|
-
by_side_link[side_link] = record
|
|
1213
|
+
by_side_link.setdefault(side_link, []).append(record)
|
|
741
1214
|
return by_side_link
|
|
742
1215
|
|
|
743
1216
|
@staticmethod
|
|
744
|
-
def
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
Map a list of record models to a list reverse side links of a given type. The reverse side links must already
|
|
748
|
-
be loaded.
|
|
749
|
-
|
|
750
|
-
:param models: A list of record models.
|
|
751
|
-
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
752
|
-
located.
|
|
753
|
-
:param side_link_type: The record model wrapper of the reverse side links.
|
|
754
|
-
:return: A dict[ModelType, list[SideLink]]. If an input model doesn't have reverse side links of the given type,
|
|
755
|
-
then it will map to an empty list.
|
|
756
|
-
"""
|
|
757
|
-
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
758
|
-
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
759
|
-
for model in models:
|
|
760
|
-
return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
|
|
761
|
-
return return_dict
|
|
762
|
-
|
|
763
|
-
@staticmethod
|
|
764
|
-
def map_to_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
765
|
-
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
|
|
1217
|
+
def map_to_reverse_side_link(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
|
|
1218
|
+
side_link_type: type[WrappedType] | str) \
|
|
1219
|
+
-> dict[IsRecordModel, WrappedType | PyRecordModel]:
|
|
766
1220
|
"""
|
|
767
1221
|
Map a list of record models to the reverse side link of a given type. If a given record has more than one
|
|
768
1222
|
reverse side link of this type, an exception is thrown. The reverse side links must already be loaded.
|
|
@@ -770,14 +1224,18 @@ class RecordHandler:
|
|
|
770
1224
|
:param models: A list of record models.
|
|
771
1225
|
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
772
1226
|
located.
|
|
773
|
-
:param side_link_type: The record model wrapper of the reverse side links.
|
|
1227
|
+
:param side_link_type: The record model wrapper or data type name of the reverse side links. If a data type
|
|
1228
|
+
name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
774
1229
|
:return: A dict[ModelType, SideLink]. If an input model doesn't have reverse side links of the given type,
|
|
775
1230
|
then it will map to None.
|
|
776
1231
|
"""
|
|
777
1232
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
778
|
-
return_dict: dict[
|
|
1233
|
+
return_dict: dict[RecordModel, WrappedType | PyRecordModel] = {}
|
|
779
1234
|
for model in models:
|
|
780
|
-
|
|
1235
|
+
if isinstance(side_link_type, str):
|
|
1236
|
+
links: list[WrappedType] = model.get(ReverseSideLink.of(side_link_type, field_name))
|
|
1237
|
+
else:
|
|
1238
|
+
links: list[WrappedType] = model.get(ReverseSideLink.of_type(side_link_type, field_name))
|
|
781
1239
|
if len(links) > 1:
|
|
782
1240
|
raise SapioException(f"Model {model.data_type_name} {model.record_id} has more than one reverse link "
|
|
783
1241
|
f"of type {side_link_type.get_wrapper_data_type_name()}.")
|
|
@@ -785,32 +1243,34 @@ class RecordHandler:
|
|
|
785
1243
|
return return_dict
|
|
786
1244
|
|
|
787
1245
|
@staticmethod
|
|
788
|
-
def
|
|
789
|
-
side_link_type: type[WrappedType]
|
|
1246
|
+
def map_to_reverse_side_links(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
|
|
1247
|
+
side_link_type: type[WrappedType] | str) \
|
|
1248
|
+
-> dict[IsRecordModel, list[WrappedType] | list[PyRecordModel]]:
|
|
790
1249
|
"""
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
The reverse side links must already be loaded.
|
|
1250
|
+
Map a list of record models to a list reverse side links of a given type. The reverse side links must already
|
|
1251
|
+
be loaded.
|
|
794
1252
|
|
|
795
1253
|
:param models: A list of record models.
|
|
796
1254
|
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
797
1255
|
located.
|
|
798
|
-
:param side_link_type: The record model wrapper of the reverse side links.
|
|
799
|
-
|
|
800
|
-
|
|
1256
|
+
:param side_link_type: The record model wrapper or data type name of the reverse side links. If a data type
|
|
1257
|
+
name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
1258
|
+
:return: A dict[ModelType, list[SideLink]]. If an input model doesn't have reverse side links of the given type,
|
|
1259
|
+
then it will map to an empty list.
|
|
801
1260
|
"""
|
|
802
1261
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
return
|
|
1262
|
+
return_dict: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = {}
|
|
1263
|
+
for model in models:
|
|
1264
|
+
if isinstance(side_link_type, str):
|
|
1265
|
+
return_dict[model] = model.get(ReverseSideLink.of(side_link_type, field_name))
|
|
1266
|
+
else:
|
|
1267
|
+
return_dict[model] = model.get(ReverseSideLink.of_type(side_link_type, field_name))
|
|
1268
|
+
return return_dict
|
|
810
1269
|
|
|
811
1270
|
@staticmethod
|
|
812
|
-
def map_by_reverse_side_link(models: Iterable[
|
|
813
|
-
side_link_type: type[WrappedType]
|
|
1271
|
+
def map_by_reverse_side_link(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
|
|
1272
|
+
side_link_type: type[WrappedType] | str) \
|
|
1273
|
+
-> dict[WrappedType | PyRecordModel, IsRecordModel]:
|
|
814
1274
|
"""
|
|
815
1275
|
Take a list of record models and map them by their reverse side link. Essentially an inversion of
|
|
816
1276
|
map_to_reverse_side_link. If two records share the same reverse side link, an exception is thrown.
|
|
@@ -819,14 +1279,15 @@ class RecordHandler:
|
|
|
819
1279
|
:param models: A list of record models.
|
|
820
1280
|
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
821
1281
|
located.
|
|
822
|
-
:param side_link_type: The record model wrapper of the reverse side links.
|
|
1282
|
+
:param side_link_type: The record model wrapper or data type name of the reverse side links. If a data type
|
|
1283
|
+
name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
823
1284
|
:return: A dict[SideLink, ModelType]. If an input model doesn't have a reverse side link of the given type
|
|
824
1285
|
pointing to it, then it will not be in the resulting dictionary.
|
|
825
1286
|
"""
|
|
826
1287
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
827
|
-
to_side_link: dict[
|
|
1288
|
+
to_side_link: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler\
|
|
828
1289
|
.map_to_reverse_side_link(models, field_name, side_link_type)
|
|
829
|
-
by_side_link: dict[WrappedType,
|
|
1290
|
+
by_side_link: dict[WrappedType | PyRecordModel, RecordModel] = {}
|
|
830
1291
|
for record, side_link in to_side_link.items():
|
|
831
1292
|
if side_link is None:
|
|
832
1293
|
continue
|
|
@@ -837,144 +1298,35 @@ class RecordHandler:
|
|
|
837
1298
|
return by_side_link
|
|
838
1299
|
|
|
839
1300
|
@staticmethod
|
|
840
|
-
def
|
|
841
|
-
|
|
842
|
-
Map the given records their record IDs.
|
|
843
|
-
|
|
844
|
-
:param models: The records to map.
|
|
845
|
-
:return: A dict mapping the record ID to each record.
|
|
846
|
-
"""
|
|
847
|
-
ret_dict: dict[int, SapioRecord] = {}
|
|
848
|
-
for model in models:
|
|
849
|
-
ret_dict.update({model.record_id: model})
|
|
850
|
-
return ret_dict
|
|
851
|
-
|
|
852
|
-
@staticmethod
|
|
853
|
-
def map_by_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
|
|
854
|
-
-> dict[FieldValue, list[SapioRecord]]:
|
|
855
|
-
"""
|
|
856
|
-
Map the given records by one of their fields. If any two records share the same field value, they'll appear in
|
|
857
|
-
the same value list.
|
|
858
|
-
|
|
859
|
-
:param models: The records to map.
|
|
860
|
-
:param field_name: The field name to map against.
|
|
861
|
-
:return: A dict mapping field values to the records with that value.
|
|
862
|
-
"""
|
|
863
|
-
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
864
|
-
ret_dict: dict[FieldValue, list[SapioRecord]] = {}
|
|
865
|
-
for model in models:
|
|
866
|
-
val: FieldValue = model.get_field_value(field_name)
|
|
867
|
-
ret_dict.setdefault(val, []).append(model)
|
|
868
|
-
return ret_dict
|
|
869
|
-
|
|
870
|
-
@staticmethod
|
|
871
|
-
def map_by_unique_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
|
|
872
|
-
-> dict[FieldValue, SapioRecord]:
|
|
873
|
-
"""
|
|
874
|
-
Uniquely map the given records by one of their fields. If any two records share the same field value, throws
|
|
875
|
-
an exception.
|
|
876
|
-
|
|
877
|
-
:param models: The records to map.
|
|
878
|
-
:param field_name: The field name to map against.
|
|
879
|
-
:return: A dict mapping field values to the record with that value.
|
|
880
|
-
"""
|
|
881
|
-
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
882
|
-
ret_dict: dict[FieldValue, SapioRecord] = {}
|
|
883
|
-
for model in models:
|
|
884
|
-
val: FieldValue = model.get_field_value(field_name)
|
|
885
|
-
if val in ret_dict:
|
|
886
|
-
raise SapioException(f"Value {val} encountered more than once in models list.")
|
|
887
|
-
ret_dict.update({val: model})
|
|
888
|
-
return ret_dict
|
|
889
|
-
|
|
890
|
-
@staticmethod
|
|
891
|
-
def sum_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
892
|
-
"""
|
|
893
|
-
Sum up the numeric value of a given field across all input models. Excepts that all given models have a value.
|
|
894
|
-
If the field is an integer field, the value will be converted to a float.
|
|
895
|
-
|
|
896
|
-
:param models: The models to calculate the sum of.
|
|
897
|
-
:param field_name: The name of the numeric field to sum.
|
|
898
|
-
:return: The sum of the field values for the collection of models.
|
|
899
|
-
"""
|
|
900
|
-
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
901
|
-
field_sum: float = 0
|
|
902
|
-
for model in models:
|
|
903
|
-
field_sum += float(model.get_field_value(field_name))
|
|
904
|
-
return field_sum
|
|
905
|
-
|
|
906
|
-
@staticmethod
|
|
907
|
-
def mean_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
908
|
-
"""
|
|
909
|
-
Calculate the mean of the numeric value of a given field across all input models. Excepts that all given models
|
|
910
|
-
have a value. If the field is an integer field, the value will be converted to a float.
|
|
911
|
-
|
|
912
|
-
:param models: The models to calculate the mean of.
|
|
913
|
-
:param field_name: The name of the numeric field to mean.
|
|
914
|
-
:return: The mean of the field values for the collection of models.
|
|
915
|
-
"""
|
|
916
|
-
return RecordHandler.sum_of_field(models, field_name) / len(list(models))
|
|
917
|
-
|
|
918
|
-
@staticmethod
|
|
919
|
-
def get_newest_record(records: Iterable[SapioRecord]) -> SapioRecord:
|
|
920
|
-
"""
|
|
921
|
-
Get the newest record from a list of records.
|
|
922
|
-
|
|
923
|
-
:param records: The list of records.
|
|
924
|
-
:return: The input record with the highest record ID. None if the input list is empty.
|
|
925
|
-
"""
|
|
926
|
-
newest: SapioRecord | None = None
|
|
927
|
-
for record in records:
|
|
928
|
-
if newest is None or record.record_id > newest.record_id:
|
|
929
|
-
newest = record
|
|
930
|
-
return newest
|
|
931
|
-
|
|
932
|
-
# FR-46696: Add a function for getting the oldest record in a list, just like we have one for the newest record.
|
|
933
|
-
@staticmethod
|
|
934
|
-
def get_oldest_record(records: Iterable[SapioRecord]) -> SapioRecord:
|
|
935
|
-
"""
|
|
936
|
-
Get the oldest record from a list of records.
|
|
937
|
-
|
|
938
|
-
:param records: The list of records.
|
|
939
|
-
:return: The input record with the lowest record ID. None if the input list is empty.
|
|
940
|
-
"""
|
|
941
|
-
oldest: SapioRecord | None = None
|
|
942
|
-
for record in records:
|
|
943
|
-
if oldest is None or record.record_id < oldest.record_id:
|
|
944
|
-
oldest = record
|
|
945
|
-
return oldest
|
|
946
|
-
|
|
947
|
-
@staticmethod
|
|
948
|
-
def values_to_field_maps(field_name: FieldIdentifier, values: Iterable[FieldValue],
|
|
949
|
-
existing_fields: list[FieldMap] | None = None) -> list[FieldMap]:
|
|
1301
|
+
def map_by_reverse_side_links(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
|
|
1302
|
+
side_link_type: type[WrappedType] | str) -> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
|
|
950
1303
|
"""
|
|
951
|
-
|
|
1304
|
+
Take a list of record models and map them by their reverse side links. Essentially an inversion of
|
|
1305
|
+
map_to_reverse_side_links. Input models that share a reverse side link will end up in the same list.
|
|
1306
|
+
The reverse side links must already be loaded.
|
|
952
1307
|
|
|
953
|
-
:param
|
|
954
|
-
:param
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1308
|
+
:param models: A list of record models.
|
|
1309
|
+
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
1310
|
+
located.
|
|
1311
|
+
:param side_link_type: The record model wrapper or data type name of the reverse side links. If a data type
|
|
1312
|
+
name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
1313
|
+
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
|
|
1314
|
+
pointing to it, then it will not be in the resulting dictionary.
|
|
958
1315
|
"""
|
|
959
|
-
# Update the existing fields map list if one is given.
|
|
960
1316
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
field.update({field_name: value})
|
|
969
|
-
return existing_fields
|
|
970
|
-
# Otherwise, create a new fields map list.
|
|
971
|
-
return [{field_name: value} for value in values]
|
|
1317
|
+
to_side_links: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler\
|
|
1318
|
+
.map_to_reverse_side_links(models, field_name, side_link_type)
|
|
1319
|
+
by_side_links: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
1320
|
+
for record, side_links in to_side_links.items():
|
|
1321
|
+
for side_link in side_links:
|
|
1322
|
+
by_side_links.setdefault(side_link, []).append(record)
|
|
1323
|
+
return by_side_links
|
|
972
1324
|
|
|
973
1325
|
# FR-46155: Update relationship path traversing functions to be non-static and take in a wrapper type so that the
|
|
974
1326
|
# output can be wrapped instead of requiring the user to wrap the output.
|
|
975
|
-
def get_linear_path(self, models: Iterable[
|
|
1327
|
+
def get_linear_path(self, models: Iterable[IsRecordModel], path: RelationshipPath,
|
|
976
1328
|
wrapper_type: type[WrappedType] | None = None) \
|
|
977
|
-
-> dict[
|
|
1329
|
+
-> dict[IsRecordModel, WrappedType | PyRecordModel | None]:
|
|
978
1330
|
"""
|
|
979
1331
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
980
1332
|
path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
|
|
@@ -987,7 +1339,7 @@ class RecordHandler:
|
|
|
987
1339
|
:return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
|
|
988
1340
|
path couldn't be reached, the record will map to None.
|
|
989
1341
|
"""
|
|
990
|
-
ret_dict: dict[RecordModel, WrappedType | None] = {}
|
|
1342
|
+
ret_dict: dict[RecordModel, WrappedType | PyRecordModel | None] = {}
|
|
991
1343
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
992
1344
|
path: list[RelationshipNode] = path.path
|
|
993
1345
|
for model in models:
|
|
@@ -1034,9 +1386,9 @@ class RecordHandler:
|
|
|
1034
1386
|
ret_dict.update({model: self.wrap_model(current, wrapper_type) if current else None})
|
|
1035
1387
|
return ret_dict
|
|
1036
1388
|
|
|
1037
|
-
def get_branching_path(self, models: Iterable[
|
|
1389
|
+
def get_branching_path(self, models: Iterable[IsRecordModel], path: RelationshipPath,
|
|
1038
1390
|
wrapper_type: type[WrappedType] | None = None)\
|
|
1039
|
-
-> dict[
|
|
1391
|
+
-> dict[IsRecordModel, list[WrappedType] | list[PyRecordModel]]:
|
|
1040
1392
|
"""
|
|
1041
1393
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
1042
1394
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
@@ -1049,7 +1401,7 @@ class RecordHandler:
|
|
|
1049
1401
|
:return: Each record model mapped to the records at the end of the path starting from itself. If the end of the
|
|
1050
1402
|
path couldn't be reached, the record will map to an empty list.
|
|
1051
1403
|
"""
|
|
1052
|
-
ret_dict: dict[RecordModel, list[WrappedType]] = {}
|
|
1404
|
+
ret_dict: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = {}
|
|
1053
1405
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
1054
1406
|
path: list[RelationshipNode] = path.path
|
|
1055
1407
|
for model in models:
|
|
@@ -1071,7 +1423,9 @@ class RecordHandler:
|
|
|
1071
1423
|
elif direction == RelationshipNodeType.DESCENDANT:
|
|
1072
1424
|
next_search.update(self.an_man.get_descendant_of_type(search, data_type))
|
|
1073
1425
|
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
1074
|
-
|
|
1426
|
+
side_link: RecordModel | None = search.get_forward_side_link(node.data_field_name)
|
|
1427
|
+
if side_link:
|
|
1428
|
+
next_search.add(side_link)
|
|
1075
1429
|
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
1076
1430
|
next_search.update(search.get_reverse_side_link(data_type, node.data_field_name))
|
|
1077
1431
|
else:
|
|
@@ -1083,9 +1437,9 @@ class RecordHandler:
|
|
|
1083
1437
|
|
|
1084
1438
|
# FR-46155: Create a relationship traversing function that returns a single function at the end of the path like
|
|
1085
1439
|
# get_linear_path but can handle branching paths in the middle of the search like get_branching_path.
|
|
1086
|
-
def get_flat_path(self, models: Iterable[
|
|
1440
|
+
def get_flat_path(self, models: Iterable[IsRecordModel], path: RelationshipPath,
|
|
1087
1441
|
wrapper_type: type[WrappedType] | None = None) \
|
|
1088
|
-
-> dict[
|
|
1442
|
+
-> dict[IsRecordModel, WrappedType | PyRecordModel | None]:
|
|
1089
1443
|
"""
|
|
1090
1444
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
1091
1445
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
@@ -1102,7 +1456,7 @@ class RecordHandler:
|
|
|
1102
1456
|
:return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
|
|
1103
1457
|
path couldn't be reached, the record will map to None.
|
|
1104
1458
|
"""
|
|
1105
|
-
ret_dict: dict[RecordModel, WrappedType | None] = {}
|
|
1459
|
+
ret_dict: dict[RecordModel, WrappedType | PyRecordModel | None] = {}
|
|
1106
1460
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
1107
1461
|
path: list[RelationshipNode] = path.path
|
|
1108
1462
|
for model in models:
|
|
@@ -1121,7 +1475,8 @@ class RecordHandler:
|
|
|
1121
1475
|
elif direction == RelationshipNodeType.DESCENDANT:
|
|
1122
1476
|
current = list(self.an_man.get_descendant_of_type(current[0], data_type))
|
|
1123
1477
|
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
1124
|
-
|
|
1478
|
+
side_link: RecordModel | None = current[0].get_forward_side_link(node.data_field_name)
|
|
1479
|
+
current = [side_link] if side_link else []
|
|
1125
1480
|
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
1126
1481
|
current = current[0].get_reverse_side_link(data_type, node.data_field_name)
|
|
1127
1482
|
else:
|
|
@@ -1169,3 +1524,42 @@ class RecordHandler:
|
|
|
1169
1524
|
if record_type != model_type:
|
|
1170
1525
|
raise SapioException(f"Data record of type {record_type} cannot be wrapped by the record model wrapper "
|
|
1171
1526
|
f"of type {model_type}")
|
|
1527
|
+
|
|
1528
|
+
@staticmethod
|
|
1529
|
+
def _spoof_child_load(model: RecordModel, child: RecordModel) -> None:
|
|
1530
|
+
"""
|
|
1531
|
+
Spoof the loading of a child record on a record model. This is useful for when you have records that you know
|
|
1532
|
+
are related but didn't use the relationship manager to load the relationship, which would make a webservice
|
|
1533
|
+
call.
|
|
1534
|
+
"""
|
|
1535
|
+
RecordHandler._spoof_children_load(model, [child])
|
|
1536
|
+
|
|
1537
|
+
@staticmethod
|
|
1538
|
+
def _spoof_children_load(model: RecordModel, children: list[RecordModel]) -> None:
|
|
1539
|
+
"""
|
|
1540
|
+
Spoof the loading of child records on a record model. This is useful for when you have records that you know
|
|
1541
|
+
are related but didn't use the relationship manager to load the relationship, which would make a webservice
|
|
1542
|
+
"""
|
|
1543
|
+
model: PyRecordModel = RecordModelInstanceManager.unwrap(model)
|
|
1544
|
+
child_dt: str = AliasUtil.to_singular_data_type_name(children)
|
|
1545
|
+
# noinspection PyProtectedMember
|
|
1546
|
+
model._mark_children_loaded(child_dt, RecordModelInstanceManager.unwrap_list(children))
|
|
1547
|
+
|
|
1548
|
+
@staticmethod
|
|
1549
|
+
def _spoof_parent_load(model: RecordModel, parent: RecordModel) -> None:
|
|
1550
|
+
"""
|
|
1551
|
+
Spoof the loading of a parent record on a record model. This is useful for when you have records that you know
|
|
1552
|
+
are related but didn't use the relationship manager to load the relationship, which would make a webservice
|
|
1553
|
+
"""
|
|
1554
|
+
RecordHandler._spoof_parents_load(model, [parent])
|
|
1555
|
+
|
|
1556
|
+
@staticmethod
|
|
1557
|
+
def _spoof_parents_load(model: RecordModel, parents: list[RecordModel]) -> None:
|
|
1558
|
+
"""
|
|
1559
|
+
Spoof the loading of parent records on a record model. This is useful for when you have records that you know
|
|
1560
|
+
are related but didn't use the relationship manager to load the relationship, which would make a webservice
|
|
1561
|
+
"""
|
|
1562
|
+
model: PyRecordModel = RecordModelInstanceManager.unwrap(model)
|
|
1563
|
+
parent_dt: str = AliasUtil.to_singular_data_type_name(parents)
|
|
1564
|
+
# noinspection PyProtectedMember
|
|
1565
|
+
model._mark_children_loaded(parent_dt, RecordModelInstanceManager.unwrap_list(parents))
|