sapiopycommons 2025.7.17a612__py3-none-any.whl → 2025.7.18a614__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/__init__.py +0 -0
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.py +43 -0
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.pyi +31 -0
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2_grpc.py +24 -0
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.py +123 -0
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.pyi +598 -0
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2_grpc.py +24 -0
- sapiopycommons/ai/api/plan/proto/step_output_pb2.py +45 -0
- sapiopycommons/ai/api/plan/proto/step_output_pb2.pyi +42 -0
- sapiopycommons/ai/api/plan/proto/step_output_pb2_grpc.py +24 -0
- sapiopycommons/ai/api/plan/proto/step_pb2.py +43 -0
- sapiopycommons/ai/api/plan/proto/step_pb2.pyi +43 -0
- sapiopycommons/ai/api/plan/proto/step_pb2_grpc.py +24 -0
- sapiopycommons/ai/api/plan/script/proto/script_pb2.py +55 -0
- sapiopycommons/ai/api/plan/script/proto/script_pb2.pyi +115 -0
- sapiopycommons/ai/api/plan/script/proto/script_pb2_grpc.py +153 -0
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2.py +57 -0
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2.pyi +96 -0
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2_grpc.py +24 -0
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2.py +69 -0
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2.pyi +232 -0
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2_grpc.py +154 -0
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.py +39 -0
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.pyi +32 -0
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2_grpc.py +24 -0
- sapiopycommons/ai/protobuf_utils.py +508 -0
- sapiopycommons/ai/test_client.py +251 -0
- sapiopycommons/ai/tool_service_base.py +826 -0
- sapiopycommons/callbacks/callback_util.py +332 -665
- sapiopycommons/callbacks/field_builder.py +0 -2
- sapiopycommons/chem/IndigoMolecules.py +1 -29
- sapiopycommons/chem/Molecules.py +3 -3
- sapiopycommons/customreport/auto_pagers.py +1 -26
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/pseudo_data_types.py +326 -349
- sapiopycommons/eln/experiment_handler.py +767 -408
- sapiopycommons/eln/experiment_report_util.py +6 -11
- sapiopycommons/eln/plate_designer.py +2 -7
- sapiopycommons/files/file_util.py +5 -7
- sapiopycommons/general/accession_service.py +2 -2
- sapiopycommons/general/aliases.py +1 -3
- sapiopycommons/general/audit_log.py +0 -7
- sapiopycommons/general/custom_report_util.py +0 -12
- sapiopycommons/processtracking/custom_workflow_handler.py +1 -11
- sapiopycommons/processtracking/endpoints.py +0 -27
- sapiopycommons/recordmodel/record_handler.py +391 -785
- sapiopycommons/rules/eln_rule_handler.py +1 -8
- sapiopycommons/rules/on_save_rule_handler.py +1 -8
- sapiopycommons/webhook/webhook_handlers.py +4 -9
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.7.17a612.dist-info → sapiopycommons-2025.7.18a614.dist-info}/METADATA +2 -2
- sapiopycommons-2025.7.18a614.dist-info/RECORD +92 -0
- sapiopycommons/eln/experiment_cache.py +0 -188
- sapiopycommons/eln/experiment_step_factory.py +0 -476
- sapiopycommons/eln/step_creation.py +0 -236
- sapiopycommons/general/data_structure_util.py +0 -115
- sapiopycommons-2025.7.17a612.dist-info/RECORD +0 -68
- {sapiopycommons-2025.7.17a612.dist-info → sapiopycommons-2025.7.18a614.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.7.17a612.dist-info → sapiopycommons-2025.7.18a614.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import io
|
|
4
3
|
import warnings
|
|
5
4
|
from collections.abc import Iterable
|
|
6
|
-
from typing import Collection, TypeVar, TypeAlias
|
|
7
5
|
from weakref import WeakValueDictionary
|
|
8
6
|
|
|
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
|
|
13
7
|
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
14
8
|
from sapiopylib.rest.User import SapioUser
|
|
15
9
|
from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, RawReportTerm, ReportColumn
|
|
@@ -19,35 +13,21 @@ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
|
19
13
|
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
20
14
|
from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
|
|
21
15
|
QueryAllRecordsOfTypeAutoPager
|
|
22
|
-
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
23
|
-
RecordModelPropertyType, AbstractRecordModelPropertyAdder, AbstractRecordModelPropertySetter, \
|
|
24
|
-
AbstractRecordModelPropertyRemover
|
|
16
|
+
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
25
17
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager, \
|
|
26
18
|
RecordModelRelationshipManager
|
|
27
19
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, WrappedRecordModel
|
|
28
20
|
from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
|
|
29
21
|
RelationshipNodeType
|
|
30
22
|
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
31
|
-
from sapiopylib.rest.utils.recordmodel.properties import Parents, Parent, Children, Child, ForwardSideLink, \
|
|
32
|
-
ReverseSideLink
|
|
33
23
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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."""
|
|
24
|
+
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
|
|
25
|
+
FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey
|
|
26
|
+
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
27
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
47
28
|
|
|
48
29
|
|
|
49
30
|
# 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.
|
|
51
31
|
class RecordHandler:
|
|
52
32
|
"""
|
|
53
33
|
A collection of shorthand methods for dealing with the various record managers.
|
|
@@ -78,11 +58,12 @@ class RecordHandler:
|
|
|
78
58
|
"""
|
|
79
59
|
:param context: The current webhook context or a user object to send requests from.
|
|
80
60
|
"""
|
|
61
|
+
self.user = AliasUtil.to_sapio_user(context)
|
|
81
62
|
if self.__initialized:
|
|
82
63
|
return
|
|
83
64
|
self.__initialized = True
|
|
84
65
|
|
|
85
|
-
self.user =
|
|
66
|
+
self.user = context if isinstance(context, SapioUser) else context.user
|
|
86
67
|
self.dr_man = DataRecordManager(self.user)
|
|
87
68
|
self.rec_man = RecordModelManager(self.user)
|
|
88
69
|
self.inst_man = self.rec_man.instance_manager
|
|
@@ -122,172 +103,9 @@ class RecordHandler:
|
|
|
122
103
|
"""
|
|
123
104
|
return [self.wrap_model(x, wrapper_type) for x in records]
|
|
124
105
|
|
|
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
|
-
|
|
286
106
|
# 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.
|
|
288
107
|
def query_models(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
|
|
289
|
-
value_list: Iterable[FieldValue] |
|
|
290
|
-
page_limit: int | None = None, page_size: int | None = None) \
|
|
108
|
+
value_list: Iterable[FieldValue], page_limit: int | None = None, page_size: int | None = None) \
|
|
291
109
|
-> list[WrappedType] | list[PyRecordModel]:
|
|
292
110
|
"""
|
|
293
111
|
Shorthand for using the data record manager to query for a list of data records by field value
|
|
@@ -295,9 +113,7 @@ class RecordHandler:
|
|
|
295
113
|
|
|
296
114
|
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
297
115
|
:param field: The field to query on.
|
|
298
|
-
:param value_list: The values of the field to query on
|
|
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.
|
|
116
|
+
:param value_list: The values of the field to query on.
|
|
301
117
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
302
118
|
only functions if you set a page size or the platform enforces a page size.
|
|
303
119
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
@@ -310,10 +126,8 @@ class RecordHandler:
|
|
|
310
126
|
return self.query_models_with_criteria(wrapper_type, field, value_list, criteria, page_limit)[0]
|
|
311
127
|
|
|
312
128
|
def query_and_map_models(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
|
|
313
|
-
value_list: Iterable[FieldValue] |
|
|
314
|
-
|
|
315
|
-
*,
|
|
316
|
-
mapping_field: FieldIdentifier | None = None) \
|
|
129
|
+
value_list: Iterable[FieldValue], page_limit: int | None = None,
|
|
130
|
+
page_size: int | None = None, *, mapping_field: FieldIdentifier | None = None) \
|
|
317
131
|
-> dict[FieldValue, list[WrappedType] | list[PyRecordModel]]:
|
|
318
132
|
"""
|
|
319
133
|
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
@@ -321,9 +135,7 @@ class RecordHandler:
|
|
|
321
135
|
|
|
322
136
|
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
323
137
|
:param field: The field to query and map on.
|
|
324
|
-
:param value_list: The values of the field to query on
|
|
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.
|
|
138
|
+
:param value_list: The values of the field to query on.
|
|
327
139
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
328
140
|
only functions if you set a page size or the platform enforces a page size.
|
|
329
141
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
@@ -338,10 +150,8 @@ class RecordHandler:
|
|
|
338
150
|
mapping_field)
|
|
339
151
|
|
|
340
152
|
def query_and_unique_map_models(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
|
|
341
|
-
value_list: Iterable[FieldValue] |
|
|
342
|
-
|
|
343
|
-
*,
|
|
344
|
-
mapping_field: FieldIdentifier | None = None) \
|
|
153
|
+
value_list: Iterable[FieldValue], page_limit: int | None = None,
|
|
154
|
+
page_size: int | None = None, *, mapping_field: FieldIdentifier | None = None) \
|
|
345
155
|
-> dict[FieldValue, WrappedType | PyRecordModel]:
|
|
346
156
|
"""
|
|
347
157
|
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
@@ -350,9 +160,7 @@ class RecordHandler:
|
|
|
350
160
|
|
|
351
161
|
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
352
162
|
:param field: The field to query and map on.
|
|
353
|
-
:param value_list: The values of the field to query on
|
|
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.
|
|
163
|
+
:param value_list: The values of the field to query on.
|
|
356
164
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
357
165
|
only functions if you set a page size or the platform enforces a page size.
|
|
358
166
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
@@ -367,7 +175,7 @@ class RecordHandler:
|
|
|
367
175
|
mapping_field)
|
|
368
176
|
|
|
369
177
|
def query_models_with_criteria(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
|
|
370
|
-
value_list: Iterable[FieldValue]
|
|
178
|
+
value_list: Iterable[FieldValue],
|
|
371
179
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
372
180
|
page_limit: int | None = None) \
|
|
373
181
|
-> tuple[list[WrappedType] | list[PyRecordModel], DataRecordPojoPageCriteria]:
|
|
@@ -377,9 +185,7 @@ class RecordHandler:
|
|
|
377
185
|
|
|
378
186
|
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
379
187
|
:param field: The field to query on.
|
|
380
|
-
:param value_list: The values of the field to query on
|
|
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.
|
|
188
|
+
:param value_list: The values of the field to query on.
|
|
383
189
|
:param paging_criteria: The paging criteria to start the query with.
|
|
384
190
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
385
191
|
possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
|
|
@@ -391,8 +197,6 @@ class RecordHandler:
|
|
|
391
197
|
if isinstance(wrapper_type, str):
|
|
392
198
|
wrapper_type = None
|
|
393
199
|
field: str = AliasUtil.to_data_field_name(field)
|
|
394
|
-
if isinstance(value_list, FieldValue):
|
|
395
|
-
value_list: list[FieldValue] = [value_list]
|
|
396
200
|
pager = QueryDataRecordsAutoPager(dt, field, list(value_list), self.user, paging_criteria)
|
|
397
201
|
pager.max_page = page_limit
|
|
398
202
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
@@ -532,11 +336,9 @@ class RecordHandler:
|
|
|
532
336
|
"""
|
|
533
337
|
warnings.warn("Deprecated in favor of the [System/Custom/Quick]ReportRecordAutoPager classes.", DeprecationWarning)
|
|
534
338
|
if isinstance(report_name, str):
|
|
535
|
-
# noinspection PyDeprecation
|
|
536
339
|
results: list[dict[str, FieldValue]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
|
|
537
340
|
page_limit, page_size, page_number)
|
|
538
341
|
elif isinstance(report_name, RawReportTerm):
|
|
539
|
-
# noinspection PyDeprecation
|
|
540
342
|
results: list[dict[str, FieldValue]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
|
|
541
343
|
page_limit, page_size, page_number)
|
|
542
344
|
elif isinstance(report_name, CustomReportCriteria):
|
|
@@ -549,480 +351,224 @@ class RecordHandler:
|
|
|
549
351
|
# Enforce that the given custom report has a record ID column.
|
|
550
352
|
if not any([x.data_type_name == dt and x.data_field_name == "RecordId" for x in report_name.column_list]):
|
|
551
353
|
report_name.column_list.append(ReportColumn(dt, "RecordId", FieldType.LONG))
|
|
552
|
-
# noinspection PyDeprecation
|
|
553
354
|
results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, report_name, filters,
|
|
554
355
|
page_limit, page_size, page_number)
|
|
555
356
|
else:
|
|
556
357
|
raise SapioException("Unrecognized report object.")
|
|
557
358
|
|
|
558
359
|
# Using the bracket accessor because we want to throw an exception if RecordId doesn't exist in the report.
|
|
559
|
-
# This should only possibly be the case with system reports, as quick reports will include the record ID
|
|
360
|
+
# This should only possibly be the case with system reports, as quick reports will include the record ID and
|
|
560
361
|
# we forced any given custom report to have a record ID column.
|
|
561
362
|
ids: list[int] = [row["RecordId"] for row in results]
|
|
562
363
|
return self.query_models_by_id(wrapper_type, ids)
|
|
563
364
|
|
|
564
|
-
|
|
565
|
-
def map_by_id(models: Iterable[IsSapioRecord]) -> dict[int, IsSapioRecord]:
|
|
566
|
-
"""
|
|
567
|
-
Map the given records their record IDs.
|
|
568
|
-
|
|
569
|
-
:param models: The records to map.
|
|
570
|
-
:return: A dict mapping the record ID to each record.
|
|
571
|
-
"""
|
|
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
|
|
576
|
-
|
|
577
|
-
@staticmethod
|
|
578
|
-
def map_by_field(models: Iterable[IsSapioRecord], field_name: FieldIdentifier) \
|
|
579
|
-
-> dict[FieldValue, list[IsSapioRecord]]:
|
|
580
|
-
"""
|
|
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.
|
|
583
|
-
|
|
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.
|
|
587
|
-
"""
|
|
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
|
|
594
|
-
|
|
595
|
-
@staticmethod
|
|
596
|
-
def map_by_unique_field(models: Iterable[IsSapioRecord], field_name: FieldIdentifier) \
|
|
597
|
-
-> dict[FieldValue, IsSapioRecord]:
|
|
598
|
-
"""
|
|
599
|
-
Uniquely map the given records by one of their fields. If any two records share the same field value, throws
|
|
600
|
-
an exception.
|
|
601
|
-
|
|
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.
|
|
605
|
-
"""
|
|
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
|
|
614
|
-
|
|
615
|
-
# FR-47525: Add functions for getting and setting record image bytes.
|
|
616
|
-
def get_record_image(self, record: SapioRecord) -> bytes:
|
|
617
|
-
"""
|
|
618
|
-
Retrieve the record image for a given record.
|
|
619
|
-
|
|
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)
|
|
627
|
-
|
|
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
|
|
633
|
-
|
|
634
|
-
def set_record_image(self, record: SapioRecord, file_data: str | bytes) -> None:
|
|
635
|
-
"""
|
|
636
|
-
Set the record image for a given record.
|
|
637
|
-
|
|
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)
|
|
644
|
-
|
|
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.
|
|
650
|
-
|
|
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.
|
|
654
|
-
"""
|
|
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
|
|
662
|
-
|
|
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.
|
|
668
|
-
|
|
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.
|
|
672
|
-
"""
|
|
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:
|
|
365
|
+
def add_model(self, wrapper_type: type[WrappedType] | str) -> WrappedType | PyRecordModel:
|
|
698
366
|
"""
|
|
699
|
-
|
|
367
|
+
Shorthand for using the instance manager to add a new record model of the given type.
|
|
700
368
|
|
|
701
|
-
:param
|
|
702
|
-
:
|
|
703
|
-
|
|
369
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the record.
|
|
370
|
+
:return: The newly added record model. If a data type name was used instead of a model wrapper, then the
|
|
371
|
+
returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
704
372
|
"""
|
|
705
|
-
|
|
706
|
-
return min(records, key=lambda x: x.get_field_value(field))
|
|
373
|
+
return self.add_models(wrapper_type, 1)[0]
|
|
707
374
|
|
|
708
|
-
|
|
709
|
-
def get_max_record(records: list[IsSapioRecord], field: FieldIdentifier) -> IsSapioRecord:
|
|
375
|
+
def add_models(self, wrapper_type: type[WrappedType] | str, num: int) -> list[WrappedType] | list[PyRecordModel]:
|
|
710
376
|
"""
|
|
711
|
-
|
|
377
|
+
Shorthand for using the instance manager to add new record models of the given type.
|
|
712
378
|
|
|
713
|
-
:param
|
|
714
|
-
:param
|
|
715
|
-
:return: The record
|
|
379
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
380
|
+
:param num: The number of models to create.
|
|
381
|
+
:return: The newly added record models. If a data type name was used instead of a model wrapper, then the
|
|
382
|
+
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
716
383
|
"""
|
|
717
|
-
|
|
718
|
-
|
|
384
|
+
if isinstance(wrapper_type, str):
|
|
385
|
+
return self.inst_man.add_new_records(wrapper_type, num)
|
|
386
|
+
return self.inst_man.add_new_records_of_type(num, wrapper_type)
|
|
719
387
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
def get_values_list(records: list[RecordModel], field: FieldIdentifier) -> list[FieldValue]:
|
|
388
|
+
def add_models_with_data(self, wrapper_type: type[WrappedType] | str, fields: list[FieldIdentifierMap]) \
|
|
389
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
723
390
|
"""
|
|
724
|
-
|
|
391
|
+
Shorthand for using the instance manager to add new models of the given type, and then initializing all those
|
|
392
|
+
models with the given fields.
|
|
725
393
|
|
|
726
|
-
:param
|
|
727
|
-
:param
|
|
728
|
-
:return:
|
|
729
|
-
|
|
394
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
395
|
+
:param fields: A list of field maps to initialize the record models with.
|
|
396
|
+
:return: The newly added record models with the provided fields set. The records will be in the same order as
|
|
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.
|
|
730
399
|
"""
|
|
731
|
-
|
|
732
|
-
|
|
400
|
+
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
401
|
+
models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
|
|
402
|
+
for model, field_list in zip(models, fields):
|
|
403
|
+
model.set_field_values(field_list)
|
|
404
|
+
return models
|
|
733
405
|
|
|
734
|
-
|
|
735
|
-
|
|
406
|
+
def find_or_add_model(self, wrapper_type: type[WrappedType] | str, primary_identifier: FieldIdentifier,
|
|
407
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
|
|
408
|
+
-> WrappedType | PyRecordModel:
|
|
736
409
|
"""
|
|
737
|
-
|
|
410
|
+
Find a unique record that matches the given field values. If no such records exist, add a record model to the
|
|
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.
|
|
738
413
|
|
|
739
|
-
|
|
740
|
-
|
|
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}
|
|
414
|
+
The record is searched for using the primary identifier field name and value. If multiple records are returned
|
|
415
|
+
by the query on this primary identifier, then the secondary identifiers are used to filter the results.
|
|
745
416
|
|
|
746
|
-
|
|
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.
|
|
417
|
+
Makes a webservice call to query for the existing record.
|
|
750
418
|
|
|
751
|
-
:param
|
|
752
|
-
:param
|
|
753
|
-
:param
|
|
419
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the record.
|
|
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.
|
|
754
427
|
"""
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
428
|
+
# PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
|
|
429
|
+
# If no secondary identifiers were provided, use an empty dictionary.
|
|
430
|
+
if secondary_identifiers is None:
|
|
431
|
+
secondary_identifiers = {}
|
|
758
432
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
433
|
+
primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
|
|
434
|
+
secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
|
|
435
|
+
unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
|
|
436
|
+
secondary_identifiers)
|
|
437
|
+
# If a unique record matched the identifiers, return it.
|
|
438
|
+
if unique_record is not None:
|
|
439
|
+
return unique_record
|
|
764
440
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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]
|
|
441
|
+
# If none of the results matched the identifiers, create a new record with all identifiers set.
|
|
442
|
+
# Put the primary identifier and value into the secondary identifiers list and use that as the fields map
|
|
443
|
+
# for this new record.
|
|
444
|
+
secondary_identifiers.update({primary_identifier: id_value})
|
|
445
|
+
return self.add_models_with_data(wrapper_type, [secondary_identifiers])[0]
|
|
784
446
|
|
|
785
|
-
|
|
786
|
-
def get_from_all(records: Iterable[RecordModel], getter: _PropertyGetter[_PropertyType]) \
|
|
787
|
-
-> list[RecordModelPropertyType]:
|
|
447
|
+
def create_models(self, wrapper_type: type[WrappedType] | str, num: int) -> list[WrappedType] | list[PyRecordModel]:
|
|
788
448
|
"""
|
|
789
|
-
|
|
790
|
-
record models
|
|
791
|
-
record.
|
|
449
|
+
Shorthand for creating new records via the data record manager and then returning them as wrapped
|
|
450
|
+
record models. Useful in cases where your record model needs to have a valid record ID.
|
|
792
451
|
|
|
793
|
-
|
|
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]
|
|
452
|
+
Makes a webservice call to create the data records.
|
|
799
453
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
454
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
455
|
+
:param num: The number of new records to create.
|
|
456
|
+
:return: The newly created record models. If a data type name was used instead of a model wrapper, then the
|
|
457
|
+
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
803
458
|
"""
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
459
|
+
dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
460
|
+
if isinstance(wrapper_type, str):
|
|
461
|
+
wrapper_type = None
|
|
462
|
+
return self.wrap_models(self.dr_man.add_data_records(dt, num), wrapper_type)
|
|
807
463
|
|
|
808
|
-
|
|
809
|
-
|
|
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.
|
|
464
|
+
def create_models_with_data(self, wrapper_type: type[WrappedType] | str, fields: list[FieldIdentifierMap]) \
|
|
465
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
812
466
|
"""
|
|
813
|
-
|
|
467
|
+
Shorthand for creating new records via the data record manager with field data to initialize the records with
|
|
468
|
+
and then returning them as wrapped record models. Useful in cases where your record model needs to have a valid
|
|
469
|
+
record ID.
|
|
814
470
|
|
|
815
|
-
|
|
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.
|
|
471
|
+
Makes a webservice call to create the data records.
|
|
821
472
|
|
|
822
|
-
:param
|
|
823
|
-
:param
|
|
824
|
-
:return:
|
|
825
|
-
|
|
473
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
474
|
+
:param fields: The field map list to initialize the new data records with.
|
|
475
|
+
:return: The newly created record models. If a data type name was used instead of a model wrapper, then the
|
|
476
|
+
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
826
477
|
"""
|
|
827
|
-
|
|
478
|
+
dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
479
|
+
if isinstance(wrapper_type, str):
|
|
480
|
+
wrapper_type = None
|
|
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)
|
|
828
483
|
|
|
829
|
-
|
|
830
|
-
|
|
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) \
|
|
484
|
+
def find_or_create_model(self, wrapper_type: type[WrappedType] | str, primary_identifier: FieldIdentifier,
|
|
485
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
|
|
871
486
|
-> WrappedType | PyRecordModel:
|
|
872
487
|
"""
|
|
873
|
-
|
|
874
|
-
|
|
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
|
|
488
|
+
Find a unique record that matches the given field values. If no such records exist, create one with the
|
|
489
|
+
identifying fields set to the desired values. If more than one record with the identifying values exists,
|
|
490
|
+
throws an exception.
|
|
928
491
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
parents, and remove any existing parents that are not in the input list.
|
|
492
|
+
The record is searched for using the primary identifier field name and value. If multiple records are returned
|
|
493
|
+
by the query on this primary identifier, then the secondary identifiers are used to filter the results.
|
|
494
|
+
|
|
495
|
+
Makes a webservice call to query for the existing record. Makes an additional webservice call if the record
|
|
496
|
+
needs to be created.
|
|
935
497
|
|
|
936
|
-
:param
|
|
937
|
-
:param
|
|
938
|
-
:param
|
|
498
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the record.
|
|
499
|
+
:param primary_identifier: The data field name of the field to search on.
|
|
500
|
+
:param id_value: The value of the identifying field to search for.
|
|
501
|
+
:param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
|
|
502
|
+
the primary identifier.
|
|
503
|
+
:return: The record model with the identifying field value, either pulled from the system or newly created.
|
|
504
|
+
If a data type name was used instead of a model wrapper, then the returned record will be a PyRecordModel
|
|
505
|
+
instead of a WrappedRecordModel.
|
|
939
506
|
"""
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
507
|
+
# PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
|
|
508
|
+
# If no secondary identifiers were provided, use an empty dictionary.
|
|
509
|
+
if secondary_identifiers is None:
|
|
510
|
+
secondary_identifiers = {}
|
|
511
|
+
|
|
512
|
+
primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
|
|
513
|
+
secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
|
|
514
|
+
unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
|
|
515
|
+
secondary_identifiers)
|
|
516
|
+
# If a unique record matched the identifiers, return it.
|
|
517
|
+
if unique_record is not None:
|
|
518
|
+
return unique_record
|
|
519
|
+
|
|
520
|
+
# If none of the results matched the identifiers, create a new record with all identifiers set.
|
|
521
|
+
# Put the primary identifier and value into the secondary identifiers list and use that as the fields map
|
|
522
|
+
# for this new record.
|
|
523
|
+
secondary_identifiers.update({primary_identifier: id_value})
|
|
524
|
+
return self.create_models_with_data(wrapper_type, [secondary_identifiers])[0]
|
|
948
525
|
|
|
949
526
|
@staticmethod
|
|
950
|
-
def
|
|
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]:
|
|
527
|
+
def map_to_parent(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType])\
|
|
528
|
+
-> dict[WrappedRecordModel, WrappedType]:
|
|
974
529
|
"""
|
|
975
530
|
Map a list of record models to a single parent of a given type. The parents must already be loaded.
|
|
976
531
|
|
|
977
532
|
:param models: A list of record models.
|
|
978
|
-
:param parent_type: The record model wrapper
|
|
979
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
533
|
+
:param parent_type: The record model wrapper of the parent.
|
|
980
534
|
:return: A dict[ModelType, ParentType]. If an input model doesn't have a parent of the given parent type, then
|
|
981
535
|
it will map to None.
|
|
982
536
|
"""
|
|
983
|
-
return_dict: dict[
|
|
537
|
+
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
984
538
|
for model in models:
|
|
985
|
-
|
|
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))
|
|
539
|
+
return_dict[model] = model.get_parent_of_type(parent_type)
|
|
989
540
|
return return_dict
|
|
990
541
|
|
|
991
542
|
@staticmethod
|
|
992
|
-
def map_to_parents(models: Iterable[
|
|
993
|
-
-> dict[
|
|
543
|
+
def map_to_parents(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
|
|
544
|
+
-> dict[WrappedRecordModel, list[WrappedType]]:
|
|
994
545
|
"""
|
|
995
546
|
Map a list of record models to a list parents of a given type. The parents must already be loaded.
|
|
996
547
|
|
|
997
548
|
:param models: A list of record models.
|
|
998
|
-
:param parent_type: The record model wrapper
|
|
999
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
549
|
+
:param parent_type: The record model wrapper of the parents.
|
|
1000
550
|
:return: A dict[ModelType, list[ParentType]]. If an input model doesn't have a parent of the given parent type,
|
|
1001
551
|
then it will map to an empty list.
|
|
1002
552
|
"""
|
|
1003
|
-
return_dict: dict[WrappedRecordModel, list[WrappedType]
|
|
553
|
+
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
1004
554
|
for model in models:
|
|
1005
|
-
|
|
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))
|
|
555
|
+
return_dict[model] = model.get_parents_of_type(parent_type)
|
|
1009
556
|
return return_dict
|
|
1010
557
|
|
|
1011
558
|
@staticmethod
|
|
1012
|
-
def map_by_parent(models: Iterable[
|
|
1013
|
-
-> dict[WrappedType
|
|
559
|
+
def map_by_parent(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
|
|
560
|
+
-> dict[WrappedType, WrappedRecordModel]:
|
|
1014
561
|
"""
|
|
1015
562
|
Take a list of record models and map them by their parent. Essentially an inversion of map_to_parent.
|
|
1016
563
|
If two records share the same parent, an exception will be thrown. The parents must already be loaded.
|
|
1017
564
|
|
|
1018
565
|
:param models: A list of record models.
|
|
1019
|
-
:param parent_type: The record model wrapper
|
|
1020
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
566
|
+
:param parent_type: The record model wrapper of the parents.
|
|
1021
567
|
:return: A dict[ParentType, ModelType]. If an input model doesn't have a parent of the given parent type,
|
|
1022
568
|
then it will not be in the resulting dictionary.
|
|
1023
569
|
"""
|
|
1024
|
-
to_parent: dict[
|
|
1025
|
-
by_parent: dict[WrappedType
|
|
570
|
+
to_parent: dict[WrappedRecordModel, WrappedType] = RecordHandler.map_to_parent(models, parent_type)
|
|
571
|
+
by_parent: dict[WrappedType, WrappedRecordModel] = {}
|
|
1026
572
|
for record, parent in to_parent.items():
|
|
1027
573
|
if parent is None:
|
|
1028
574
|
continue
|
|
@@ -1033,81 +579,70 @@ class RecordHandler:
|
|
|
1033
579
|
return by_parent
|
|
1034
580
|
|
|
1035
581
|
@staticmethod
|
|
1036
|
-
def map_by_parents(models: Iterable[
|
|
1037
|
-
-> dict[WrappedType
|
|
582
|
+
def map_by_parents(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
|
|
583
|
+
-> dict[WrappedType, list[WrappedRecordModel]]:
|
|
1038
584
|
"""
|
|
1039
585
|
Take a list of record models and map them by their parents. Essentially an inversion of map_to_parents. Input
|
|
1040
586
|
models that share a parent will end up in the same list. The parents must already be loaded.
|
|
1041
587
|
|
|
1042
588
|
:param models: A list of record models.
|
|
1043
|
-
:param parent_type: The record model wrapper
|
|
1044
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
589
|
+
:param parent_type: The record model wrapper of the parents.
|
|
1045
590
|
:return: A dict[ParentType, list[ModelType]]. If an input model doesn't have a parent of the given parent type,
|
|
1046
591
|
then it will not be in the resulting dictionary.
|
|
1047
592
|
"""
|
|
1048
|
-
to_parents: dict[
|
|
1049
|
-
|
|
1050
|
-
by_parents: dict[WrappedType | PyRecordModel, list[RecordModel]] = {}
|
|
593
|
+
to_parents: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler.map_to_parents(models, parent_type)
|
|
594
|
+
by_parents: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
1051
595
|
for record, parents in to_parents.items():
|
|
1052
596
|
for parent in parents:
|
|
1053
597
|
by_parents.setdefault(parent, []).append(record)
|
|
1054
598
|
return by_parents
|
|
1055
599
|
|
|
1056
600
|
@staticmethod
|
|
1057
|
-
def map_to_child(models: Iterable[
|
|
1058
|
-
-> dict[
|
|
601
|
+
def map_to_child(models: Iterable[WrappedRecordModel], child_type: type[WrappedType])\
|
|
602
|
+
-> dict[WrappedRecordModel, WrappedType]:
|
|
1059
603
|
"""
|
|
1060
604
|
Map a list of record models to a single child of a given type. The children must already be loaded.
|
|
1061
605
|
|
|
1062
606
|
:param models: A list of record models.
|
|
1063
|
-
:param child_type: The record model wrapper
|
|
1064
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
607
|
+
:param child_type: The record model wrapper of the child.
|
|
1065
608
|
:return: A dict[ModelType, ChildType]. If an input model doesn't have a child of the given child type, then
|
|
1066
609
|
it will map to None.
|
|
1067
610
|
"""
|
|
1068
|
-
return_dict: dict[
|
|
611
|
+
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
1069
612
|
for model in models:
|
|
1070
|
-
|
|
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))
|
|
613
|
+
return_dict[model] = model.get_child_of_type(child_type)
|
|
1074
614
|
return return_dict
|
|
1075
615
|
|
|
1076
616
|
@staticmethod
|
|
1077
|
-
def map_to_children(models: Iterable[
|
|
1078
|
-
-> dict[
|
|
617
|
+
def map_to_children(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
|
|
618
|
+
-> dict[WrappedRecordModel, list[WrappedType]]:
|
|
1079
619
|
"""
|
|
1080
620
|
Map a list of record models to a list children of a given type. The children must already be loaded.
|
|
1081
621
|
|
|
1082
622
|
:param models: A list of record models.
|
|
1083
|
-
:param child_type: The record model wrapper
|
|
1084
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
623
|
+
:param child_type: The record model wrapper of the children.
|
|
1085
624
|
:return: A dict[ModelType, list[ChildType]]. If an input model doesn't have children of the given child type,
|
|
1086
625
|
then it will map to an empty list.
|
|
1087
626
|
"""
|
|
1088
|
-
return_dict: dict[
|
|
627
|
+
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
1089
628
|
for model in models:
|
|
1090
|
-
|
|
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))
|
|
629
|
+
return_dict[model] = model.get_children_of_type(child_type)
|
|
1094
630
|
return return_dict
|
|
1095
631
|
|
|
1096
632
|
@staticmethod
|
|
1097
|
-
def map_by_child(models: Iterable[
|
|
1098
|
-
-> dict[WrappedType
|
|
633
|
+
def map_by_child(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
|
|
634
|
+
-> dict[WrappedType, WrappedRecordModel]:
|
|
1099
635
|
"""
|
|
1100
636
|
Take a list of record models and map them by their children. Essentially an inversion of map_to_child.
|
|
1101
637
|
If two records share the same child, an exception will be thrown. The children must already be loaded.
|
|
1102
638
|
|
|
1103
639
|
:param models: A list of record models.
|
|
1104
|
-
:param child_type: The record model wrapper
|
|
1105
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
640
|
+
:param child_type: The record model wrapper of the children.
|
|
1106
641
|
:return: A dict[ChildType, ModelType]. If an input model doesn't have a child of the given child type,
|
|
1107
642
|
then it will not be in the resulting dictionary.
|
|
1108
643
|
"""
|
|
1109
|
-
to_child: dict[
|
|
1110
|
-
by_child: dict[WrappedType
|
|
644
|
+
to_child: dict[WrappedRecordModel, WrappedType] = RecordHandler.map_to_child(models, child_type)
|
|
645
|
+
by_child: dict[WrappedType, WrappedRecordModel] = {}
|
|
1111
646
|
for record, child in to_child.items():
|
|
1112
647
|
if child is None:
|
|
1113
648
|
continue
|
|
@@ -1118,105 +653,116 @@ class RecordHandler:
|
|
|
1118
653
|
return by_child
|
|
1119
654
|
|
|
1120
655
|
@staticmethod
|
|
1121
|
-
def map_by_children(models: Iterable[
|
|
1122
|
-
-> dict[WrappedType
|
|
656
|
+
def map_by_children(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
|
|
657
|
+
-> dict[WrappedType, list[WrappedRecordModel]]:
|
|
1123
658
|
"""
|
|
1124
659
|
Take a list of record models and map them by their children. Essentially an inversion of map_to_children. Input
|
|
1125
660
|
models that share a child will end up in the same list. The children must already be loaded.
|
|
1126
661
|
|
|
1127
662
|
:param models: A list of record models.
|
|
1128
|
-
:param child_type: The record model wrapper
|
|
1129
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
663
|
+
:param child_type: The record model wrapper of the children.
|
|
1130
664
|
:return: A dict[ChildType, list[ModelType]]. If an input model doesn't have children of the given child type,
|
|
1131
665
|
then it will not be in the resulting dictionary.
|
|
1132
666
|
"""
|
|
1133
|
-
to_children: dict[
|
|
1134
|
-
|
|
1135
|
-
by_children: dict[WrappedType | PyRecordModel, list[RecordModel]] = {}
|
|
667
|
+
to_children: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler.map_to_children(models, child_type)
|
|
668
|
+
by_children: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
1136
669
|
for record, children in to_children.items():
|
|
1137
670
|
for child in children:
|
|
1138
671
|
by_children.setdefault(child, []).append(record)
|
|
1139
672
|
return by_children
|
|
1140
673
|
|
|
1141
674
|
@staticmethod
|
|
1142
|
-
def map_to_forward_side_link(models: Iterable[
|
|
1143
|
-
side_link_type: type[WrappedType]
|
|
1144
|
-
-> dict[IsRecordModel, WrappedType | PyRecordModel]:
|
|
675
|
+
def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
676
|
+
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
|
|
1145
677
|
"""
|
|
1146
678
|
Map a list of record models to their forward side link. The forward side link must already be loaded.
|
|
1147
679
|
|
|
1148
680
|
:param models: A list of record models.
|
|
1149
681
|
:param field_name: The field name on the record models where the side link is located.
|
|
1150
|
-
:param side_link_type: The record model wrapper of the forward side link.
|
|
1151
|
-
be returned as PyRecordModels instead of WrappedRecordModels.
|
|
682
|
+
:param side_link_type: The record model wrapper of the forward side link.
|
|
1152
683
|
:return: A dict[ModelType, SlideLink]. If an input model doesn't have a forward side link of the given type,
|
|
1153
684
|
then it will map to None.
|
|
1154
685
|
"""
|
|
1155
686
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1156
|
-
return_dict: dict[
|
|
687
|
+
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
1157
688
|
for model in models:
|
|
1158
|
-
return_dict[model] = model.
|
|
689
|
+
return_dict[model] = model.get_forward_side_link(field_name, side_link_type)
|
|
1159
690
|
return return_dict
|
|
1160
691
|
|
|
1161
692
|
@staticmethod
|
|
1162
|
-
def
|
|
1163
|
-
|
|
1164
|
-
-> dict[WrappedType | PyRecordModel, IsRecordModel]:
|
|
693
|
+
def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
694
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
1165
695
|
"""
|
|
1166
696
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
1167
|
-
map_to_forward_side_link
|
|
697
|
+
map_to_forward_side_link. Input models that share a forward side link will end up in the same list.
|
|
1168
698
|
The forward side link must already be loaded.
|
|
1169
699
|
|
|
1170
700
|
:param models: A list of record models.
|
|
1171
701
|
:param field_name: The field name on the record models where the side link is located.
|
|
1172
|
-
:param side_link_type: The record model wrapper of the forward side links.
|
|
1173
|
-
|
|
1174
|
-
:return: A dict[SideLink, ModelType]. If an input model doesn't have a forward side link of the given type
|
|
702
|
+
:param side_link_type: The record model wrapper of the forward side links.
|
|
703
|
+
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
|
|
1175
704
|
pointing to it, then it will not be in the resulting dictionary.
|
|
1176
705
|
"""
|
|
1177
706
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1178
|
-
to_side_link: dict[
|
|
707
|
+
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
1179
708
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
1180
|
-
by_side_link: dict[WrappedType
|
|
709
|
+
by_side_link: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
1181
710
|
for record, side_link in to_side_link.items():
|
|
1182
711
|
if side_link is None:
|
|
1183
712
|
continue
|
|
1184
|
-
|
|
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
|
|
713
|
+
by_side_link.setdefault(side_link, []).append(record)
|
|
1188
714
|
return by_side_link
|
|
1189
715
|
|
|
1190
716
|
@staticmethod
|
|
1191
|
-
def
|
|
1192
|
-
|
|
1193
|
-
-> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
|
|
717
|
+
def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
718
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
|
|
1194
719
|
"""
|
|
1195
720
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
1196
|
-
map_to_forward_side_link
|
|
721
|
+
map_to_forward_side_link, but if two records share the same forward link, an exception is thrown.
|
|
1197
722
|
The forward side link must already be loaded.
|
|
1198
723
|
|
|
1199
724
|
:param models: A list of record models.
|
|
1200
725
|
:param field_name: The field name on the record models where the side link is located.
|
|
1201
|
-
:param side_link_type: The record model wrapper of the forward side links.
|
|
1202
|
-
|
|
1203
|
-
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
|
|
726
|
+
:param side_link_type: The record model wrapper of the forward side links.
|
|
727
|
+
:return: A dict[SideLink, ModelType]. If an input model doesn't have a forward side link of the given type
|
|
1204
728
|
pointing to it, then it will not be in the resulting dictionary.
|
|
1205
729
|
"""
|
|
1206
730
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1207
|
-
to_side_link: dict[
|
|
731
|
+
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
1208
732
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
1209
|
-
by_side_link: dict[WrappedType
|
|
733
|
+
by_side_link: dict[WrappedType, WrappedRecordModel] = {}
|
|
1210
734
|
for record, side_link in to_side_link.items():
|
|
1211
735
|
if side_link is None:
|
|
1212
736
|
continue
|
|
1213
|
-
|
|
737
|
+
if side_link in by_side_link:
|
|
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
|
|
1214
741
|
return by_side_link
|
|
1215
742
|
|
|
1216
743
|
@staticmethod
|
|
1217
|
-
def
|
|
1218
|
-
|
|
1219
|
-
|
|
744
|
+
def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
745
|
+
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, list[WrappedType]]:
|
|
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]:
|
|
1220
766
|
"""
|
|
1221
767
|
Map a list of record models to the reverse side link of a given type. If a given record has more than one
|
|
1222
768
|
reverse side link of this type, an exception is thrown. The reverse side links must already be loaded.
|
|
@@ -1224,18 +770,14 @@ class RecordHandler:
|
|
|
1224
770
|
:param models: A list of record models.
|
|
1225
771
|
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
1226
772
|
located.
|
|
1227
|
-
:param side_link_type: The record model wrapper
|
|
1228
|
-
name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
773
|
+
:param side_link_type: The record model wrapper of the reverse side links.
|
|
1229
774
|
:return: A dict[ModelType, SideLink]. If an input model doesn't have reverse side links of the given type,
|
|
1230
775
|
then it will map to None.
|
|
1231
776
|
"""
|
|
1232
777
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1233
|
-
return_dict: dict[
|
|
778
|
+
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
1234
779
|
for model in models:
|
|
1235
|
-
|
|
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))
|
|
780
|
+
links: list[WrappedType] = model.get_reverse_side_link(field_name, side_link_type)
|
|
1239
781
|
if len(links) > 1:
|
|
1240
782
|
raise SapioException(f"Model {model.data_type_name} {model.record_id} has more than one reverse link "
|
|
1241
783
|
f"of type {side_link_type.get_wrapper_data_type_name()}.")
|
|
@@ -1243,34 +785,32 @@ class RecordHandler:
|
|
|
1243
785
|
return return_dict
|
|
1244
786
|
|
|
1245
787
|
@staticmethod
|
|
1246
|
-
def
|
|
1247
|
-
side_link_type: type[WrappedType]
|
|
1248
|
-
-> dict[IsRecordModel, list[WrappedType] | list[PyRecordModel]]:
|
|
788
|
+
def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
789
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
1249
790
|
"""
|
|
1250
|
-
|
|
1251
|
-
|
|
791
|
+
Take a list of record models and map them by their reverse side links. Essentially an inversion of
|
|
792
|
+
map_to_reverse_side_links. Input models that share a reverse side link will end up in the same list.
|
|
793
|
+
The reverse side links must already be loaded.
|
|
1252
794
|
|
|
1253
795
|
:param models: A list of record models.
|
|
1254
796
|
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
1255
797
|
located.
|
|
1256
|
-
:param side_link_type: The record model wrapper
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
then it will map to an empty list.
|
|
798
|
+
:param side_link_type: The record model wrapper of the reverse side links.
|
|
799
|
+
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
|
|
800
|
+
pointing to it, then it will not be in the resulting dictionary.
|
|
1260
801
|
"""
|
|
1261
802
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
return
|
|
803
|
+
to_side_links: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler\
|
|
804
|
+
.map_to_reverse_side_links(models, field_name, side_link_type)
|
|
805
|
+
by_side_links: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
806
|
+
for record, side_links in to_side_links.items():
|
|
807
|
+
for side_link in side_links:
|
|
808
|
+
by_side_links.setdefault(side_link, []).append(record)
|
|
809
|
+
return by_side_links
|
|
1269
810
|
|
|
1270
811
|
@staticmethod
|
|
1271
|
-
def map_by_reverse_side_link(models: Iterable[
|
|
1272
|
-
side_link_type: type[WrappedType]
|
|
1273
|
-
-> dict[WrappedType | PyRecordModel, IsRecordModel]:
|
|
812
|
+
def map_by_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
813
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
|
|
1274
814
|
"""
|
|
1275
815
|
Take a list of record models and map them by their reverse side link. Essentially an inversion of
|
|
1276
816
|
map_to_reverse_side_link. If two records share the same reverse side link, an exception is thrown.
|
|
@@ -1279,15 +819,14 @@ class RecordHandler:
|
|
|
1279
819
|
:param models: A list of record models.
|
|
1280
820
|
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
1281
821
|
located.
|
|
1282
|
-
:param side_link_type: The record model wrapper
|
|
1283
|
-
name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
822
|
+
:param side_link_type: The record model wrapper of the reverse side links.
|
|
1284
823
|
:return: A dict[SideLink, ModelType]. If an input model doesn't have a reverse side link of the given type
|
|
1285
824
|
pointing to it, then it will not be in the resulting dictionary.
|
|
1286
825
|
"""
|
|
1287
826
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1288
|
-
to_side_link: dict[
|
|
827
|
+
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
1289
828
|
.map_to_reverse_side_link(models, field_name, side_link_type)
|
|
1290
|
-
by_side_link: dict[WrappedType
|
|
829
|
+
by_side_link: dict[WrappedType, WrappedRecordModel] = {}
|
|
1291
830
|
for record, side_link in to_side_link.items():
|
|
1292
831
|
if side_link is None:
|
|
1293
832
|
continue
|
|
@@ -1298,35 +837,144 @@ class RecordHandler:
|
|
|
1298
837
|
return by_side_link
|
|
1299
838
|
|
|
1300
839
|
@staticmethod
|
|
1301
|
-
def
|
|
1302
|
-
side_link_type: type[WrappedType] | str) -> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
|
|
840
|
+
def map_by_id(models: Iterable[SapioRecord]) -> dict[int, SapioRecord]:
|
|
1303
841
|
"""
|
|
1304
|
-
|
|
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.
|
|
842
|
+
Map the given records their record IDs.
|
|
1307
843
|
|
|
1308
|
-
:param models:
|
|
1309
|
-
:
|
|
1310
|
-
|
|
1311
|
-
:
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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.
|
|
1315
862
|
"""
|
|
1316
863
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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]:
|
|
950
|
+
"""
|
|
951
|
+
Add a list of values for a specific field to a list of dictionaries pairing each value to that field name.
|
|
952
|
+
|
|
953
|
+
:param field_name: The name of the field that the values are from.
|
|
954
|
+
:param values: A list of field values.
|
|
955
|
+
:param existing_fields: An optional existing fields map list to add the new values to. Values are added in the
|
|
956
|
+
list in the same order that they appear. If no existing fields are provided, returns a new fields map list.
|
|
957
|
+
:return: A fields map list that contains the given values mapped by the given field name.
|
|
958
|
+
"""
|
|
959
|
+
# Update the existing fields map list if one is given.
|
|
960
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
961
|
+
existing_fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(existing_fields)
|
|
962
|
+
if existing_fields:
|
|
963
|
+
values = list(values)
|
|
964
|
+
# The number of new values must match the length of the existing fields list.
|
|
965
|
+
if len(values) != len(existing_fields):
|
|
966
|
+
raise SapioException(f"Length of \"{field_name}\" values does not match the existing fields length.")
|
|
967
|
+
for field, value in zip(existing_fields, values):
|
|
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]
|
|
1324
972
|
|
|
1325
973
|
# FR-46155: Update relationship path traversing functions to be non-static and take in a wrapper type so that the
|
|
1326
974
|
# output can be wrapped instead of requiring the user to wrap the output.
|
|
1327
|
-
def get_linear_path(self, models: Iterable[
|
|
975
|
+
def get_linear_path(self, models: Iterable[RecordModel], path: RelationshipPath,
|
|
1328
976
|
wrapper_type: type[WrappedType] | None = None) \
|
|
1329
|
-
-> dict[
|
|
977
|
+
-> dict[RecordModel, WrappedType | PyRecordModel | None]:
|
|
1330
978
|
"""
|
|
1331
979
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
1332
980
|
path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
|
|
@@ -1339,7 +987,7 @@ class RecordHandler:
|
|
|
1339
987
|
:return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
|
|
1340
988
|
path couldn't be reached, the record will map to None.
|
|
1341
989
|
"""
|
|
1342
|
-
ret_dict: dict[RecordModel, WrappedType |
|
|
990
|
+
ret_dict: dict[RecordModel, WrappedType | None] = {}
|
|
1343
991
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
1344
992
|
path: list[RelationshipNode] = path.path
|
|
1345
993
|
for model in models:
|
|
@@ -1386,9 +1034,9 @@ class RecordHandler:
|
|
|
1386
1034
|
ret_dict.update({model: self.wrap_model(current, wrapper_type) if current else None})
|
|
1387
1035
|
return ret_dict
|
|
1388
1036
|
|
|
1389
|
-
def get_branching_path(self, models: Iterable[
|
|
1037
|
+
def get_branching_path(self, models: Iterable[RecordModel], path: RelationshipPath,
|
|
1390
1038
|
wrapper_type: type[WrappedType] | None = None)\
|
|
1391
|
-
-> dict[
|
|
1039
|
+
-> dict[RecordModel, list[WrappedType] | list[PyRecordModel]]:
|
|
1392
1040
|
"""
|
|
1393
1041
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
1394
1042
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
@@ -1401,7 +1049,7 @@ class RecordHandler:
|
|
|
1401
1049
|
:return: Each record model mapped to the records at the end of the path starting from itself. If the end of the
|
|
1402
1050
|
path couldn't be reached, the record will map to an empty list.
|
|
1403
1051
|
"""
|
|
1404
|
-
ret_dict: dict[RecordModel, list[WrappedType]
|
|
1052
|
+
ret_dict: dict[RecordModel, list[WrappedType]] = {}
|
|
1405
1053
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
1406
1054
|
path: list[RelationshipNode] = path.path
|
|
1407
1055
|
for model in models:
|
|
@@ -1423,9 +1071,7 @@ class RecordHandler:
|
|
|
1423
1071
|
elif direction == RelationshipNodeType.DESCENDANT:
|
|
1424
1072
|
next_search.update(self.an_man.get_descendant_of_type(search, data_type))
|
|
1425
1073
|
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
1426
|
-
|
|
1427
|
-
if side_link:
|
|
1428
|
-
next_search.add(side_link)
|
|
1074
|
+
next_search.add(search.get_forward_side_link(node.data_field_name))
|
|
1429
1075
|
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
1430
1076
|
next_search.update(search.get_reverse_side_link(data_type, node.data_field_name))
|
|
1431
1077
|
else:
|
|
@@ -1437,9 +1083,9 @@ class RecordHandler:
|
|
|
1437
1083
|
|
|
1438
1084
|
# FR-46155: Create a relationship traversing function that returns a single function at the end of the path like
|
|
1439
1085
|
# get_linear_path but can handle branching paths in the middle of the search like get_branching_path.
|
|
1440
|
-
def get_flat_path(self, models: Iterable[
|
|
1086
|
+
def get_flat_path(self, models: Iterable[RecordModel], path: RelationshipPath,
|
|
1441
1087
|
wrapper_type: type[WrappedType] | None = None) \
|
|
1442
|
-
-> dict[
|
|
1088
|
+
-> dict[RecordModel, WrappedType | PyRecordModel | None]:
|
|
1443
1089
|
"""
|
|
1444
1090
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
1445
1091
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
@@ -1456,7 +1102,7 @@ class RecordHandler:
|
|
|
1456
1102
|
:return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
|
|
1457
1103
|
path couldn't be reached, the record will map to None.
|
|
1458
1104
|
"""
|
|
1459
|
-
ret_dict: dict[RecordModel, WrappedType |
|
|
1105
|
+
ret_dict: dict[RecordModel, WrappedType | None] = {}
|
|
1460
1106
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
1461
1107
|
path: list[RelationshipNode] = path.path
|
|
1462
1108
|
for model in models:
|
|
@@ -1475,8 +1121,7 @@ class RecordHandler:
|
|
|
1475
1121
|
elif direction == RelationshipNodeType.DESCENDANT:
|
|
1476
1122
|
current = list(self.an_man.get_descendant_of_type(current[0], data_type))
|
|
1477
1123
|
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
1478
|
-
|
|
1479
|
-
current = [side_link] if side_link else []
|
|
1124
|
+
current = [current[0].get_forward_side_link(node.data_field_name)]
|
|
1480
1125
|
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
1481
1126
|
current = current[0].get_reverse_side_link(data_type, node.data_field_name)
|
|
1482
1127
|
else:
|
|
@@ -1524,42 +1169,3 @@ class RecordHandler:
|
|
|
1524
1169
|
if record_type != model_type:
|
|
1525
1170
|
raise SapioException(f"Data record of type {record_type} cannot be wrapped by the record model wrapper "
|
|
1526
1171
|
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))
|