sapiopycommons 2025.5.20a539__py3-none-any.whl → 2025.5.30a548__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/tool_of_tools.py +235 -127
- sapiopycommons/general/html_formatter.py +456 -0
- sapiopycommons/general/sapio_links.py +12 -4
- sapiopycommons/recordmodel/record_handler.py +354 -344
- {sapiopycommons-2025.5.20a539.dist-info → sapiopycommons-2025.5.30a548.dist-info}/METADATA +1 -1
- {sapiopycommons-2025.5.20a539.dist-info → sapiopycommons-2025.5.30a548.dist-info}/RECORD +8 -7
- {sapiopycommons-2025.5.20a539.dist-info → sapiopycommons-2025.5.30a548.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.5.20a539.dist-info → sapiopycommons-2025.5.30a548.dist-info}/licenses/LICENSE +0 -0
|
@@ -39,6 +39,7 @@ _PropertySetter = AbstractRecordModelPropertySetter
|
|
|
39
39
|
_PropertyType = RecordModelPropertyType
|
|
40
40
|
|
|
41
41
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
42
|
+
# FR-47575 - Reordered functions so that the Java and Python versions are as close to each other as possible.
|
|
42
43
|
class RecordHandler:
|
|
43
44
|
"""
|
|
44
45
|
A collection of shorthand methods for dealing with the various record managers.
|
|
@@ -113,6 +114,167 @@ class RecordHandler:
|
|
|
113
114
|
"""
|
|
114
115
|
return [self.wrap_model(x, wrapper_type) for x in records]
|
|
115
116
|
|
|
117
|
+
def add_model(self, wrapper_type: type[WrappedType] | str) -> WrappedType | PyRecordModel:
|
|
118
|
+
"""
|
|
119
|
+
Shorthand for using the instance manager to add a new record model of the given type.
|
|
120
|
+
|
|
121
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the record.
|
|
122
|
+
:return: The newly added record model. If a data type name was used instead of a model wrapper, then the
|
|
123
|
+
returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
124
|
+
"""
|
|
125
|
+
return self.add_models(wrapper_type, 1)[0]
|
|
126
|
+
|
|
127
|
+
def add_models(self, wrapper_type: type[WrappedType] | str, num: int) -> list[WrappedType] | list[PyRecordModel]:
|
|
128
|
+
"""
|
|
129
|
+
Shorthand for using the instance manager to add new record models of the given type.
|
|
130
|
+
|
|
131
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
132
|
+
:param num: The number of models to create.
|
|
133
|
+
:return: The newly added record models. If a data type name was used instead of a model wrapper, then the
|
|
134
|
+
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
135
|
+
"""
|
|
136
|
+
if isinstance(wrapper_type, str):
|
|
137
|
+
return self.inst_man.add_new_records(wrapper_type, num)
|
|
138
|
+
return self.inst_man.add_new_records_of_type(num, wrapper_type)
|
|
139
|
+
|
|
140
|
+
def add_models_with_data(self, wrapper_type: type[WrappedType] | str, fields: list[FieldIdentifierMap]) \
|
|
141
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
142
|
+
"""
|
|
143
|
+
Shorthand for using the instance manager to add new models of the given type, and then initializing all those
|
|
144
|
+
models with the given fields.
|
|
145
|
+
|
|
146
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
147
|
+
:param fields: A list of field maps to initialize the record models with.
|
|
148
|
+
:return: The newly added record models with the provided fields set. The records will be in the same order as
|
|
149
|
+
the fields in the fields list. If a data type name was used instead of a model wrapper, then the returned
|
|
150
|
+
records will be PyRecordModels instead of WrappedRecordModels.
|
|
151
|
+
"""
|
|
152
|
+
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
153
|
+
models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
|
|
154
|
+
for model, field_list in zip(models, fields):
|
|
155
|
+
model.set_field_values(field_list)
|
|
156
|
+
return models
|
|
157
|
+
|
|
158
|
+
def find_or_add_model(self, wrapper_type: type[WrappedType] | str, primary_identifier: FieldIdentifier,
|
|
159
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
|
|
160
|
+
-> WrappedType | PyRecordModel:
|
|
161
|
+
"""
|
|
162
|
+
Find a unique record that matches the given field values. If no such records exist, add a record model to the
|
|
163
|
+
cache with the identifying fields set to the desired values. This record will be created in the system when
|
|
164
|
+
you store and commit changes. If more than one record with the identifying values exists, throws an exception.
|
|
165
|
+
|
|
166
|
+
The record is searched for using the primary identifier field name and value. If multiple records are returned
|
|
167
|
+
by the query on this primary identifier, then the secondary identifiers are used to filter the results.
|
|
168
|
+
|
|
169
|
+
Makes a webservice call to query for the existing record.
|
|
170
|
+
|
|
171
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the record.
|
|
172
|
+
:param primary_identifier: The data field name of the field to search on.
|
|
173
|
+
:param id_value: The value of the identifying field to search for.
|
|
174
|
+
:param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
|
|
175
|
+
the primary identifier.
|
|
176
|
+
:return: The record model with the identifying field value, either pulled from the system or newly created.
|
|
177
|
+
If a data type name was used instead of a model wrapper, then the returned record will be a PyRecordModel
|
|
178
|
+
instead of a WrappedRecordModel.
|
|
179
|
+
"""
|
|
180
|
+
# PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
|
|
181
|
+
# If no secondary identifiers were provided, use an empty dictionary.
|
|
182
|
+
if secondary_identifiers is None:
|
|
183
|
+
secondary_identifiers = {}
|
|
184
|
+
|
|
185
|
+
primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
|
|
186
|
+
secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
|
|
187
|
+
unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
|
|
188
|
+
secondary_identifiers)
|
|
189
|
+
# If a unique record matched the identifiers, return it.
|
|
190
|
+
if unique_record is not None:
|
|
191
|
+
return unique_record
|
|
192
|
+
|
|
193
|
+
# If none of the results matched the identifiers, create a new record with all identifiers set.
|
|
194
|
+
# Put the primary identifier and value into the secondary identifiers list and use that as the fields map
|
|
195
|
+
# for this new record.
|
|
196
|
+
secondary_identifiers.update({primary_identifier: id_value})
|
|
197
|
+
return self.add_models_with_data(wrapper_type, [secondary_identifiers])[0]
|
|
198
|
+
|
|
199
|
+
def create_models(self, wrapper_type: type[WrappedType] | str, num: int) -> list[WrappedType] | list[PyRecordModel]:
|
|
200
|
+
"""
|
|
201
|
+
Shorthand for creating new records via the data record manager and then returning them as wrapped
|
|
202
|
+
record models. Useful in cases where your record model needs to have a valid record ID.
|
|
203
|
+
|
|
204
|
+
Makes a webservice call to create the data records.
|
|
205
|
+
|
|
206
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
207
|
+
:param num: The number of new records to create.
|
|
208
|
+
:return: The newly created record models. If a data type name was used instead of a model wrapper, then the
|
|
209
|
+
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
210
|
+
"""
|
|
211
|
+
dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
212
|
+
if isinstance(wrapper_type, str):
|
|
213
|
+
wrapper_type = None
|
|
214
|
+
return self.wrap_models(self.dr_man.add_data_records(dt, num), wrapper_type)
|
|
215
|
+
|
|
216
|
+
def create_models_with_data(self, wrapper_type: type[WrappedType] | str, fields: list[FieldIdentifierMap]) \
|
|
217
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
218
|
+
"""
|
|
219
|
+
Shorthand for creating new records via the data record manager with field data to initialize the records with
|
|
220
|
+
and then returning them as wrapped record models. Useful in cases where your record model needs to have a valid
|
|
221
|
+
record ID.
|
|
222
|
+
|
|
223
|
+
Makes a webservice call to create the data records.
|
|
224
|
+
|
|
225
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the records.
|
|
226
|
+
:param fields: The field map list to initialize the new data records with.
|
|
227
|
+
:return: The newly created record models. If a data type name was used instead of a model wrapper, then the
|
|
228
|
+
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
229
|
+
"""
|
|
230
|
+
dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
231
|
+
if isinstance(wrapper_type, str):
|
|
232
|
+
wrapper_type = None
|
|
233
|
+
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
234
|
+
return self.wrap_models(self.dr_man.add_data_records_with_data(dt, fields), wrapper_type)
|
|
235
|
+
|
|
236
|
+
def find_or_create_model(self, wrapper_type: type[WrappedType] | str, primary_identifier: FieldIdentifier,
|
|
237
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
|
|
238
|
+
-> WrappedType | PyRecordModel:
|
|
239
|
+
"""
|
|
240
|
+
Find a unique record that matches the given field values. If no such records exist, create one with the
|
|
241
|
+
identifying fields set to the desired values. If more than one record with the identifying values exists,
|
|
242
|
+
throws an exception.
|
|
243
|
+
|
|
244
|
+
The record is searched for using the primary identifier field name and value. If multiple records are returned
|
|
245
|
+
by the query on this primary identifier, then the secondary identifiers are used to filter the results.
|
|
246
|
+
|
|
247
|
+
Makes a webservice call to query for the existing record. Makes an additional webservice call if the record
|
|
248
|
+
needs to be created.
|
|
249
|
+
|
|
250
|
+
:param wrapper_type: The record model wrapper to use, or the data type name of the record.
|
|
251
|
+
:param primary_identifier: The data field name of the field to search on.
|
|
252
|
+
:param id_value: The value of the identifying field to search for.
|
|
253
|
+
:param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
|
|
254
|
+
the primary identifier.
|
|
255
|
+
:return: The record model with the identifying field value, either pulled from the system or newly created.
|
|
256
|
+
If a data type name was used instead of a model wrapper, then the returned record will be a PyRecordModel
|
|
257
|
+
instead of a WrappedRecordModel.
|
|
258
|
+
"""
|
|
259
|
+
# PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
|
|
260
|
+
# If no secondary identifiers were provided, use an empty dictionary.
|
|
261
|
+
if secondary_identifiers is None:
|
|
262
|
+
secondary_identifiers = {}
|
|
263
|
+
|
|
264
|
+
primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
|
|
265
|
+
secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
|
|
266
|
+
unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
|
|
267
|
+
secondary_identifiers)
|
|
268
|
+
# If a unique record matched the identifiers, return it.
|
|
269
|
+
if unique_record is not None:
|
|
270
|
+
return unique_record
|
|
271
|
+
|
|
272
|
+
# If none of the results matched the identifiers, create a new record with all identifiers set.
|
|
273
|
+
# Put the primary identifier and value into the secondary identifiers list and use that as the fields map
|
|
274
|
+
# for this new record.
|
|
275
|
+
secondary_identifiers.update({primary_identifier: id_value})
|
|
276
|
+
return self.create_models_with_data(wrapper_type, [secondary_identifiers])[0]
|
|
277
|
+
|
|
116
278
|
# CR-47491: Support providing a data type name string to receive PyRecordModels instead of requiring a WrapperType.
|
|
117
279
|
# CR-47523: Support a singular field value being provided for the value_list parameter.
|
|
118
280
|
def query_models(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
|
|
@@ -388,196 +550,161 @@ class RecordHandler:
|
|
|
388
550
|
ids: list[int] = [row["RecordId"] for row in results]
|
|
389
551
|
return self.query_models_by_id(wrapper_type, ids)
|
|
390
552
|
|
|
391
|
-
|
|
553
|
+
@staticmethod
|
|
554
|
+
def map_by_id(models: Iterable[SapioRecord]) -> dict[int, SapioRecord]:
|
|
392
555
|
"""
|
|
393
|
-
|
|
556
|
+
Map the given records their record IDs.
|
|
394
557
|
|
|
395
|
-
:param
|
|
396
|
-
:return:
|
|
397
|
-
returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
558
|
+
:param models: The records to map.
|
|
559
|
+
:return: A dict mapping the record ID to each record.
|
|
398
560
|
"""
|
|
399
|
-
|
|
561
|
+
ret_dict: dict[int, SapioRecord] = {}
|
|
562
|
+
for model in models:
|
|
563
|
+
ret_dict.update({model.record_id: model})
|
|
564
|
+
return ret_dict
|
|
400
565
|
|
|
401
|
-
|
|
566
|
+
@staticmethod
|
|
567
|
+
def map_by_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
|
|
568
|
+
-> dict[FieldValue, list[SapioRecord]]:
|
|
402
569
|
"""
|
|
403
|
-
|
|
570
|
+
Map the given records by one of their fields. If any two records share the same field value, they'll appear in
|
|
571
|
+
the same value list.
|
|
404
572
|
|
|
405
|
-
:param
|
|
406
|
-
:param
|
|
407
|
-
:return:
|
|
408
|
-
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
573
|
+
:param models: The records to map.
|
|
574
|
+
:param field_name: The field name to map against.
|
|
575
|
+
:return: A dict mapping field values to the records with that value.
|
|
409
576
|
"""
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
577
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
578
|
+
ret_dict: dict[FieldValue, list[SapioRecord]] = {}
|
|
579
|
+
for model in models:
|
|
580
|
+
val: FieldValue = model.get_field_value(field_name)
|
|
581
|
+
ret_dict.setdefault(val, []).append(model)
|
|
582
|
+
return ret_dict
|
|
413
583
|
|
|
414
|
-
|
|
415
|
-
|
|
584
|
+
@staticmethod
|
|
585
|
+
def map_by_unique_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
|
|
586
|
+
-> dict[FieldValue, SapioRecord]:
|
|
416
587
|
"""
|
|
417
|
-
|
|
418
|
-
|
|
588
|
+
Uniquely map the given records by one of their fields. If any two records share the same field value, throws
|
|
589
|
+
an exception.
|
|
419
590
|
|
|
420
|
-
:param
|
|
421
|
-
:param
|
|
422
|
-
:return:
|
|
423
|
-
the fields in the fields list. If a data type name was used instead of a model wrapper, then the returned
|
|
424
|
-
records will be PyRecordModels instead of WrappedRecordModels.
|
|
591
|
+
:param models: The records to map.
|
|
592
|
+
:param field_name: The field name to map against.
|
|
593
|
+
:return: A dict mapping field values to the record with that value.
|
|
425
594
|
"""
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
for model
|
|
429
|
-
model.
|
|
430
|
-
|
|
595
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
596
|
+
ret_dict: dict[FieldValue, SapioRecord] = {}
|
|
597
|
+
for model in models:
|
|
598
|
+
val: FieldValue = model.get_field_value(field_name)
|
|
599
|
+
if val in ret_dict:
|
|
600
|
+
raise SapioException(f"Value {val} encountered more than once in models list.")
|
|
601
|
+
ret_dict.update({val: model})
|
|
602
|
+
return ret_dict
|
|
431
603
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
-> WrappedType | PyRecordModel:
|
|
604
|
+
# FR-47525: Add functions for getting and setting record image bytes.
|
|
605
|
+
def get_record_image(self, record: SapioRecord) -> bytes:
|
|
435
606
|
"""
|
|
436
|
-
|
|
437
|
-
cache with the identifying fields set to the desired values. This record will be created in the system when
|
|
438
|
-
you store and commit changes. If more than one record with the identifying values exists, throws an exception.
|
|
439
|
-
|
|
440
|
-
The record is searched for using the primary identifier field name and value. If multiple records are returned
|
|
441
|
-
by the query on this primary identifier, then the secondary identifiers are used to filter the results.
|
|
442
|
-
|
|
443
|
-
Makes a webservice call to query for the existing record.
|
|
607
|
+
Retrieve the record image for a given record.
|
|
444
608
|
|
|
445
|
-
:param
|
|
446
|
-
:
|
|
447
|
-
:param id_value: The value of the identifying field to search for.
|
|
448
|
-
:param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
|
|
449
|
-
the primary identifier.
|
|
450
|
-
:return: The record model with the identifying field value, either pulled from the system or newly created.
|
|
451
|
-
If a data type name was used instead of a model wrapper, then the returned record will be a PyRecordModel
|
|
452
|
-
instead of a WrappedRecordModel.
|
|
609
|
+
:param record: The record model to retrieve the image of.
|
|
610
|
+
:return: The file bytes of the given record's image.
|
|
453
611
|
"""
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
|
|
460
|
-
secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
|
|
461
|
-
unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
|
|
462
|
-
secondary_identifiers)
|
|
463
|
-
# If a unique record matched the identifiers, return it.
|
|
464
|
-
if unique_record is not None:
|
|
465
|
-
return unique_record
|
|
612
|
+
record: DataRecord = AliasUtil.to_data_record(record)
|
|
613
|
+
with io.BytesIO() as data_sink:
|
|
614
|
+
def consume_data(chunk: bytes):
|
|
615
|
+
data_sink.write(chunk)
|
|
466
616
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
return
|
|
617
|
+
self.dr_man.get_record_image(record, consume_data)
|
|
618
|
+
data_sink.flush()
|
|
619
|
+
data_sink.seek(0)
|
|
620
|
+
file_bytes = data_sink.read()
|
|
621
|
+
return file_bytes
|
|
472
622
|
|
|
473
|
-
def
|
|
623
|
+
def set_record_image(self, record: SapioRecord, file_data: str | bytes) -> None:
|
|
474
624
|
"""
|
|
475
|
-
|
|
476
|
-
record models. Useful in cases where your record model needs to have a valid record ID.
|
|
477
|
-
|
|
478
|
-
Makes a webservice call to create the data records.
|
|
625
|
+
Set the record image for a given record.
|
|
479
626
|
|
|
480
|
-
:param
|
|
481
|
-
:param
|
|
482
|
-
:return: The newly created record models. If a data type name was used instead of a model wrapper, then the
|
|
483
|
-
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
627
|
+
:param record: The record model to set the image of.
|
|
628
|
+
:param file_data: The file data of the image to set on the record.
|
|
484
629
|
"""
|
|
485
|
-
|
|
486
|
-
if isinstance(
|
|
487
|
-
|
|
488
|
-
return self.wrap_models(self.dr_man.add_data_records(dt, num), wrapper_type)
|
|
630
|
+
record: DataRecord = AliasUtil.to_data_record(record)
|
|
631
|
+
with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as stream:
|
|
632
|
+
self.dr_man.set_record_image(record, stream)
|
|
489
633
|
|
|
490
|
-
|
|
491
|
-
|
|
634
|
+
@staticmethod
|
|
635
|
+
def sum_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
492
636
|
"""
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
record ID.
|
|
496
|
-
|
|
497
|
-
Makes a webservice call to create the data records.
|
|
637
|
+
Sum up the numeric value of a given field across all input models. Excepts that all given models have a value.
|
|
638
|
+
If the field is an integer field, the value will be converted to a float.
|
|
498
639
|
|
|
499
|
-
:param
|
|
500
|
-
:param
|
|
501
|
-
:return: The
|
|
502
|
-
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
640
|
+
:param models: The models to calculate the sum of.
|
|
641
|
+
:param field_name: The name of the numeric field to sum.
|
|
642
|
+
:return: The sum of the field values for the collection of models.
|
|
503
643
|
"""
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
644
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
645
|
+
field_sum: float = 0
|
|
646
|
+
for model in models:
|
|
647
|
+
val = model.get_field_value(field_name)
|
|
648
|
+
if isinstance(val, (int, float)):
|
|
649
|
+
field_sum += float(model.get_field_value(field_name))
|
|
650
|
+
return field_sum
|
|
509
651
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
-> WrappedType | PyRecordModel:
|
|
652
|
+
@staticmethod
|
|
653
|
+
def mean_of_field(models: Collection[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
513
654
|
"""
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
throws an exception.
|
|
517
|
-
|
|
518
|
-
The record is searched for using the primary identifier field name and value. If multiple records are returned
|
|
519
|
-
by the query on this primary identifier, then the secondary identifiers are used to filter the results.
|
|
655
|
+
Calculate the average (arithmetic mean) of the numeric value of a given field across all input models. Excepts
|
|
656
|
+
that all given models have a value. If the field is an integer field, the value will be converted to a float.
|
|
520
657
|
|
|
521
|
-
|
|
522
|
-
|
|
658
|
+
:param models: The models to calculate the mean of.
|
|
659
|
+
:param field_name: The name of the numeric field to mean.
|
|
660
|
+
:return: The mean of the field values for the collection of models.
|
|
661
|
+
"""
|
|
662
|
+
return RecordHandler.sum_of_field(models, field_name) / len(models)
|
|
523
663
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
:param id_value: The value of the identifying field to search for.
|
|
527
|
-
:param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
|
|
528
|
-
the primary identifier.
|
|
529
|
-
:return: The record model with the identifying field value, either pulled from the system or newly created.
|
|
530
|
-
If a data type name was used instead of a model wrapper, then the returned record will be a PyRecordModel
|
|
531
|
-
instead of a WrappedRecordModel.
|
|
664
|
+
@staticmethod
|
|
665
|
+
def get_newest_record(records: Iterable[SapioRecord]) -> SapioRecord:
|
|
532
666
|
"""
|
|
533
|
-
|
|
534
|
-
# If no secondary identifiers were provided, use an empty dictionary.
|
|
535
|
-
if secondary_identifiers is None:
|
|
536
|
-
secondary_identifiers = {}
|
|
667
|
+
Get the newest record from a list of records.
|
|
537
668
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
# If a unique record matched the identifiers, return it.
|
|
543
|
-
if unique_record is not None:
|
|
544
|
-
return unique_record
|
|
669
|
+
:param records: The list of records.
|
|
670
|
+
:return: The input record with the highest record ID. None if the input list is empty.
|
|
671
|
+
"""
|
|
672
|
+
return max(records, key=lambda x: x.record_id)
|
|
545
673
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
674
|
+
# FR-46696: Add a function for getting the oldest record in a list, just like we have one for the newest record.
|
|
675
|
+
@staticmethod
|
|
676
|
+
def get_oldest_record(records: Iterable[SapioRecord]) -> SapioRecord:
|
|
677
|
+
"""
|
|
678
|
+
Get the oldest record from a list of records.
|
|
551
679
|
|
|
552
|
-
|
|
553
|
-
|
|
680
|
+
:param records: The list of records.
|
|
681
|
+
:return: The input record with the lowest record ID. None if the input list is empty.
|
|
554
682
|
"""
|
|
555
|
-
|
|
683
|
+
return min(records, key=lambda x: x.record_id)
|
|
556
684
|
|
|
557
|
-
|
|
558
|
-
|
|
685
|
+
@staticmethod
|
|
686
|
+
def get_min_record(records: list[RecordModel], field: FieldIdentifier) -> RecordModel:
|
|
559
687
|
"""
|
|
560
|
-
record
|
|
561
|
-
with io.BytesIO() as data_sink:
|
|
562
|
-
def consume_data(chunk: bytes):
|
|
563
|
-
data_sink.write(chunk)
|
|
688
|
+
Get the record model with the minimum value of a given field from a list of record models.
|
|
564
689
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
690
|
+
:param records: The list of record models to search through.
|
|
691
|
+
:param field: The field to find the minimum value of.
|
|
692
|
+
:return: The record model with the minimum value of the given field.
|
|
693
|
+
"""
|
|
694
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
695
|
+
return min(records, key=lambda x: x.get_field_value(field))
|
|
570
696
|
|
|
571
|
-
|
|
697
|
+
@staticmethod
|
|
698
|
+
def get_max_record(records: list[RecordModel], field: FieldIdentifier) -> RecordModel:
|
|
572
699
|
"""
|
|
573
|
-
|
|
700
|
+
Get the record model with the maximum value of a given field from a list of record models.
|
|
574
701
|
|
|
575
|
-
:param
|
|
576
|
-
:param
|
|
702
|
+
:param records: The list of record models to search through.
|
|
703
|
+
:param field: The field to find the maximum value of.
|
|
704
|
+
:return: The record model with the maximum value of the given field.
|
|
577
705
|
"""
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
self.dr_man.set_record_image(record, stream)
|
|
706
|
+
field: str = AliasUtil.to_data_field_name(field)
|
|
707
|
+
return max(records, key=lambda x: x.get_field_value(field))
|
|
581
708
|
|
|
582
709
|
# FR-47522: Add RecordHandler functions that copy from the RecordModelUtil class in our Java utilities.
|
|
583
710
|
@staticmethod
|
|
@@ -619,31 +746,34 @@ class RecordHandler:
|
|
|
619
746
|
record.set_field_value(field, value)
|
|
620
747
|
|
|
621
748
|
@staticmethod
|
|
622
|
-
def
|
|
623
|
-
|
|
624
|
-
Get the record model with the minimum value of a given field from a list of record models.
|
|
625
|
-
|
|
626
|
-
:param records: The list of record models to search through.
|
|
627
|
-
:param field: The field to find the minimum value of.
|
|
628
|
-
:return: The record model with the minimum value of the given field.
|
|
629
|
-
"""
|
|
630
|
-
field: str = AliasUtil.to_data_field_name(field)
|
|
631
|
-
return min(records, key=lambda x: x.get_field_value(field))
|
|
632
|
-
|
|
633
|
-
@staticmethod
|
|
634
|
-
def get_max_record(records: list[RecordModel], field: FieldIdentifier) -> RecordModel:
|
|
749
|
+
def values_to_field_maps(field_name: FieldIdentifier, values: Iterable[FieldValue],
|
|
750
|
+
existing_fields: list[FieldMap] | None = None) -> list[FieldMap]:
|
|
635
751
|
"""
|
|
636
|
-
|
|
752
|
+
Add a list of values for a specific field to a list of dictionaries pairing each value to that field name.
|
|
637
753
|
|
|
638
|
-
:param
|
|
639
|
-
:param
|
|
640
|
-
:
|
|
754
|
+
:param field_name: The name of the field that the values are from.
|
|
755
|
+
:param values: A list of field values.
|
|
756
|
+
:param existing_fields: An optional existing fields map list to add the new values to. Values are added in the
|
|
757
|
+
list in the same order that they appear. If no existing fields are provided, returns a new fields map list.
|
|
758
|
+
:return: A fields map list that contains the given values mapped by the given field name.
|
|
641
759
|
"""
|
|
642
|
-
|
|
643
|
-
|
|
760
|
+
# Update the existing fields map list if one is given.
|
|
761
|
+
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
762
|
+
existing_fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(existing_fields)
|
|
763
|
+
if existing_fields:
|
|
764
|
+
values = list(values)
|
|
765
|
+
# The number of new values must match the length of the existing fields list.
|
|
766
|
+
if len(values) != len(existing_fields):
|
|
767
|
+
raise SapioException(f"Length of \"{field_name}\" values does not match the existing fields length.")
|
|
768
|
+
for field, value in zip(existing_fields, values):
|
|
769
|
+
field.update({field_name: value})
|
|
770
|
+
return existing_fields
|
|
771
|
+
# Otherwise, create a new fields map list.
|
|
772
|
+
return [{field_name: value} for value in values]
|
|
644
773
|
|
|
645
774
|
@staticmethod
|
|
646
|
-
def get_from_all(records: Iterable[RecordModel], getter: _PropertyGetter[_PropertyType])
|
|
775
|
+
def get_from_all(records: Iterable[RecordModel], getter: _PropertyGetter[_PropertyType]) \
|
|
776
|
+
-> list[RecordModelPropertyType]:
|
|
647
777
|
"""
|
|
648
778
|
Use a getter property on all records in a list of record models. For example, you can iterate over a list of
|
|
649
779
|
record models using a getter of Ancestors.of_type(SampleModel) to get all the SampleModel ancestors from each
|
|
@@ -657,7 +787,8 @@ class RecordHandler:
|
|
|
657
787
|
return [x.get(getter) for x in records]
|
|
658
788
|
|
|
659
789
|
@staticmethod
|
|
660
|
-
def set_on_all(records: Iterable[RecordModel], setter: _PropertySetter[_PropertyType])
|
|
790
|
+
def set_on_all(records: Iterable[RecordModel], setter: _PropertySetter[_PropertyType]) \
|
|
791
|
+
-> list[RecordModelPropertyType]:
|
|
661
792
|
"""
|
|
662
793
|
Use a setter property on all records in a list of record models. For example, you can iterate over a list of
|
|
663
794
|
record models user a setter of ForwardSideLink.ref(field_name, record) to set a forward side link on each
|
|
@@ -671,7 +802,8 @@ class RecordHandler:
|
|
|
671
802
|
return [x.set(setter) for x in records]
|
|
672
803
|
|
|
673
804
|
@staticmethod
|
|
674
|
-
def add_to_all(records: Iterable[RecordModel], adder: _PropertyAdder[_PropertyType])
|
|
805
|
+
def add_to_all(records: Iterable[RecordModel], adder: _PropertyAdder[_PropertyType]) \
|
|
806
|
+
-> list[RecordModelPropertyType]:
|
|
675
807
|
"""
|
|
676
808
|
Use an adder property on all records in a list of record models. For example, you can iterate over a list of
|
|
677
809
|
record models using an adder of Child.create(SampleModel) to create a new SampleModel child on each record.
|
|
@@ -684,7 +816,8 @@ class RecordHandler:
|
|
|
684
816
|
return [x.add(adder) for x in records]
|
|
685
817
|
|
|
686
818
|
@staticmethod
|
|
687
|
-
def remove_from_all(records: Iterable[RecordModel], remover: _PropertyRemover[_PropertyType])
|
|
819
|
+
def remove_from_all(records: Iterable[RecordModel], remover: _PropertyRemover[_PropertyType]) \
|
|
820
|
+
-> list[RecordModelPropertyType]:
|
|
688
821
|
"""
|
|
689
822
|
Use a remover property on all records in a list of record models. For example, you can iterate over a list of
|
|
690
823
|
record models using a remover of Parents.ref(records) to remove a list of parents from each record.
|
|
@@ -989,76 +1122,56 @@ class RecordHandler:
|
|
|
989
1122
|
return return_dict
|
|
990
1123
|
|
|
991
1124
|
@staticmethod
|
|
992
|
-
def
|
|
993
|
-
|
|
1125
|
+
def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
1126
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
|
|
994
1127
|
"""
|
|
995
1128
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
996
|
-
map_to_forward_side_link
|
|
1129
|
+
map_to_forward_side_link, but if two records share the same forward link, an exception is thrown.
|
|
997
1130
|
The forward side link must already be loaded.
|
|
998
1131
|
|
|
999
1132
|
:param models: A list of record models.
|
|
1000
1133
|
:param field_name: The field name on the record models where the side link is located.
|
|
1001
1134
|
:param side_link_type: The record model wrapper of the forward side links.
|
|
1002
|
-
:return: A dict[SideLink,
|
|
1135
|
+
:return: A dict[SideLink, ModelType]. If an input model doesn't have a forward side link of the given type
|
|
1003
1136
|
pointing to it, then it will not be in the resulting dictionary.
|
|
1004
1137
|
"""
|
|
1005
1138
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1006
1139
|
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
1007
1140
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
1008
|
-
by_side_link: dict[WrappedType,
|
|
1141
|
+
by_side_link: dict[WrappedType, WrappedRecordModel] = {}
|
|
1009
1142
|
for record, side_link in to_side_link.items():
|
|
1010
1143
|
if side_link is None:
|
|
1011
1144
|
continue
|
|
1012
|
-
|
|
1145
|
+
if side_link in by_side_link:
|
|
1146
|
+
raise SapioException(f"Side link {side_link.data_type_name} {side_link.record_id} encountered more "
|
|
1147
|
+
f"than once in models list.")
|
|
1148
|
+
by_side_link[side_link] = record
|
|
1013
1149
|
return by_side_link
|
|
1014
1150
|
|
|
1015
1151
|
@staticmethod
|
|
1016
|
-
def
|
|
1017
|
-
|
|
1152
|
+
def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
1153
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
1018
1154
|
"""
|
|
1019
1155
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
1020
|
-
map_to_forward_side_link
|
|
1156
|
+
map_to_forward_side_link. Input models that share a forward side link will end up in the same list.
|
|
1021
1157
|
The forward side link must already be loaded.
|
|
1022
1158
|
|
|
1023
1159
|
:param models: A list of record models.
|
|
1024
1160
|
:param field_name: The field name on the record models where the side link is located.
|
|
1025
1161
|
:param side_link_type: The record model wrapper of the forward side links.
|
|
1026
|
-
:return: A dict[SideLink, ModelType]. If an input model doesn't have a forward side link of the given type
|
|
1162
|
+
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
|
|
1027
1163
|
pointing to it, then it will not be in the resulting dictionary.
|
|
1028
1164
|
"""
|
|
1029
1165
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1030
1166
|
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
1031
1167
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
1032
|
-
by_side_link: dict[WrappedType, WrappedRecordModel] = {}
|
|
1168
|
+
by_side_link: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
1033
1169
|
for record, side_link in to_side_link.items():
|
|
1034
1170
|
if side_link is None:
|
|
1035
1171
|
continue
|
|
1036
|
-
|
|
1037
|
-
raise SapioException(f"Side link {side_link.data_type_name} {side_link.record_id} encountered more "
|
|
1038
|
-
f"than once in models list.")
|
|
1039
|
-
by_side_link[side_link] = record
|
|
1172
|
+
by_side_link.setdefault(side_link, []).append(record)
|
|
1040
1173
|
return by_side_link
|
|
1041
1174
|
|
|
1042
|
-
@staticmethod
|
|
1043
|
-
def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
1044
|
-
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, list[WrappedType]]:
|
|
1045
|
-
"""
|
|
1046
|
-
Map a list of record models to a list reverse side links of a given type. The reverse side links must already
|
|
1047
|
-
be loaded.
|
|
1048
|
-
|
|
1049
|
-
:param models: A list of record models.
|
|
1050
|
-
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
1051
|
-
located.
|
|
1052
|
-
:param side_link_type: The record model wrapper of the reverse side links.
|
|
1053
|
-
:return: A dict[ModelType, list[SideLink]]. If an input model doesn't have reverse side links of the given type,
|
|
1054
|
-
then it will map to an empty list.
|
|
1055
|
-
"""
|
|
1056
|
-
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1057
|
-
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
1058
|
-
for model in models:
|
|
1059
|
-
return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
|
|
1060
|
-
return return_dict
|
|
1061
|
-
|
|
1062
1175
|
@staticmethod
|
|
1063
1176
|
def map_to_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
1064
1177
|
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
|
|
@@ -1084,28 +1197,24 @@ class RecordHandler:
|
|
|
1084
1197
|
return return_dict
|
|
1085
1198
|
|
|
1086
1199
|
@staticmethod
|
|
1087
|
-
def
|
|
1088
|
-
side_link_type: type[WrappedType]) -> dict[
|
|
1200
|
+
def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
1201
|
+
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, list[WrappedType]]:
|
|
1089
1202
|
"""
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
The reverse side links must already be loaded.
|
|
1203
|
+
Map a list of record models to a list reverse side links of a given type. The reverse side links must already
|
|
1204
|
+
be loaded.
|
|
1093
1205
|
|
|
1094
1206
|
:param models: A list of record models.
|
|
1095
1207
|
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
1096
1208
|
located.
|
|
1097
1209
|
:param side_link_type: The record model wrapper of the reverse side links.
|
|
1098
|
-
:return: A dict[
|
|
1099
|
-
|
|
1210
|
+
:return: A dict[ModelType, list[SideLink]]. If an input model doesn't have reverse side links of the given type,
|
|
1211
|
+
then it will map to an empty list.
|
|
1100
1212
|
"""
|
|
1101
1213
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
for side_link in side_links:
|
|
1107
|
-
by_side_links.setdefault(side_link, []).append(record)
|
|
1108
|
-
return by_side_links
|
|
1214
|
+
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
1215
|
+
for model in models:
|
|
1216
|
+
return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
|
|
1217
|
+
return return_dict
|
|
1109
1218
|
|
|
1110
1219
|
@staticmethod
|
|
1111
1220
|
def map_by_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
@@ -1136,130 +1245,28 @@ class RecordHandler:
|
|
|
1136
1245
|
return by_side_link
|
|
1137
1246
|
|
|
1138
1247
|
@staticmethod
|
|
1139
|
-
def
|
|
1140
|
-
|
|
1141
|
-
Map the given records their record IDs.
|
|
1142
|
-
|
|
1143
|
-
:param models: The records to map.
|
|
1144
|
-
:return: A dict mapping the record ID to each record.
|
|
1145
|
-
"""
|
|
1146
|
-
ret_dict: dict[int, SapioRecord] = {}
|
|
1147
|
-
for model in models:
|
|
1148
|
-
ret_dict.update({model.record_id: model})
|
|
1149
|
-
return ret_dict
|
|
1150
|
-
|
|
1151
|
-
@staticmethod
|
|
1152
|
-
def map_by_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
|
|
1153
|
-
-> dict[FieldValue, list[SapioRecord]]:
|
|
1154
|
-
"""
|
|
1155
|
-
Map the given records by one of their fields. If any two records share the same field value, they'll appear in
|
|
1156
|
-
the same value list.
|
|
1157
|
-
|
|
1158
|
-
:param models: The records to map.
|
|
1159
|
-
:param field_name: The field name to map against.
|
|
1160
|
-
:return: A dict mapping field values to the records with that value.
|
|
1161
|
-
"""
|
|
1162
|
-
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1163
|
-
ret_dict: dict[FieldValue, list[SapioRecord]] = {}
|
|
1164
|
-
for model in models:
|
|
1165
|
-
val: FieldValue = model.get_field_value(field_name)
|
|
1166
|
-
ret_dict.setdefault(val, []).append(model)
|
|
1167
|
-
return ret_dict
|
|
1168
|
-
|
|
1169
|
-
@staticmethod
|
|
1170
|
-
def map_by_unique_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
|
|
1171
|
-
-> dict[FieldValue, SapioRecord]:
|
|
1172
|
-
"""
|
|
1173
|
-
Uniquely map the given records by one of their fields. If any two records share the same field value, throws
|
|
1174
|
-
an exception.
|
|
1175
|
-
|
|
1176
|
-
:param models: The records to map.
|
|
1177
|
-
:param field_name: The field name to map against.
|
|
1178
|
-
:return: A dict mapping field values to the record with that value.
|
|
1179
|
-
"""
|
|
1180
|
-
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1181
|
-
ret_dict: dict[FieldValue, SapioRecord] = {}
|
|
1182
|
-
for model in models:
|
|
1183
|
-
val: FieldValue = model.get_field_value(field_name)
|
|
1184
|
-
if val in ret_dict:
|
|
1185
|
-
raise SapioException(f"Value {val} encountered more than once in models list.")
|
|
1186
|
-
ret_dict.update({val: model})
|
|
1187
|
-
return ret_dict
|
|
1188
|
-
|
|
1189
|
-
@staticmethod
|
|
1190
|
-
def sum_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
1191
|
-
"""
|
|
1192
|
-
Sum up the numeric value of a given field across all input models. Excepts that all given models have a value.
|
|
1193
|
-
If the field is an integer field, the value will be converted to a float.
|
|
1194
|
-
|
|
1195
|
-
:param models: The models to calculate the sum of.
|
|
1196
|
-
:param field_name: The name of the numeric field to sum.
|
|
1197
|
-
:return: The sum of the field values for the collection of models.
|
|
1198
|
-
"""
|
|
1199
|
-
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1200
|
-
field_sum: float = 0
|
|
1201
|
-
for model in models:
|
|
1202
|
-
field_sum += float(model.get_field_value(field_name))
|
|
1203
|
-
return field_sum
|
|
1204
|
-
|
|
1205
|
-
@staticmethod
|
|
1206
|
-
def mean_of_field(models: Collection[SapioRecord], field_name: FieldIdentifier) -> float:
|
|
1207
|
-
"""
|
|
1208
|
-
Calculate the mean of the numeric value of a given field across all input models. Excepts that all given models
|
|
1209
|
-
have a value. If the field is an integer field, the value will be converted to a float.
|
|
1210
|
-
|
|
1211
|
-
:param models: The models to calculate the mean of.
|
|
1212
|
-
:param field_name: The name of the numeric field to mean.
|
|
1213
|
-
:return: The mean of the field values for the collection of models.
|
|
1214
|
-
"""
|
|
1215
|
-
return RecordHandler.sum_of_field(models, field_name) / len(models)
|
|
1216
|
-
|
|
1217
|
-
@staticmethod
|
|
1218
|
-
def get_newest_record(records: Iterable[SapioRecord]) -> SapioRecord:
|
|
1219
|
-
"""
|
|
1220
|
-
Get the newest record from a list of records.
|
|
1221
|
-
|
|
1222
|
-
:param records: The list of records.
|
|
1223
|
-
:return: The input record with the highest record ID. None if the input list is empty.
|
|
1224
|
-
"""
|
|
1225
|
-
return max(records, key=lambda x: x.record_id)
|
|
1226
|
-
|
|
1227
|
-
# FR-46696: Add a function for getting the oldest record in a list, just like we have one for the newest record.
|
|
1228
|
-
@staticmethod
|
|
1229
|
-
def get_oldest_record(records: Iterable[SapioRecord]) -> SapioRecord:
|
|
1230
|
-
"""
|
|
1231
|
-
Get the oldest record from a list of records.
|
|
1232
|
-
|
|
1233
|
-
:param records: The list of records.
|
|
1234
|
-
:return: The input record with the lowest record ID. None if the input list is empty.
|
|
1235
|
-
"""
|
|
1236
|
-
return min(records, key=lambda x: x.record_id)
|
|
1237
|
-
|
|
1238
|
-
@staticmethod
|
|
1239
|
-
def values_to_field_maps(field_name: FieldIdentifier, values: Iterable[FieldValue],
|
|
1240
|
-
existing_fields: list[FieldMap] | None = None) -> list[FieldMap]:
|
|
1248
|
+
def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
1249
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
1241
1250
|
"""
|
|
1242
|
-
|
|
1251
|
+
Take a list of record models and map them by their reverse side links. Essentially an inversion of
|
|
1252
|
+
map_to_reverse_side_links. Input models that share a reverse side link will end up in the same list.
|
|
1253
|
+
The reverse side links must already be loaded.
|
|
1243
1254
|
|
|
1244
|
-
:param
|
|
1245
|
-
:param
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
:return: A
|
|
1255
|
+
:param models: A list of record models.
|
|
1256
|
+
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
1257
|
+
located.
|
|
1258
|
+
:param side_link_type: The record model wrapper of the reverse side links.
|
|
1259
|
+
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
|
|
1260
|
+
pointing to it, then it will not be in the resulting dictionary.
|
|
1249
1261
|
"""
|
|
1250
|
-
# Update the existing fields map list if one is given.
|
|
1251
1262
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
field.update({field_name: value})
|
|
1260
|
-
return existing_fields
|
|
1261
|
-
# Otherwise, create a new fields map list.
|
|
1262
|
-
return [{field_name: value} for value in values]
|
|
1263
|
+
to_side_links: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler\
|
|
1264
|
+
.map_to_reverse_side_links(models, field_name, side_link_type)
|
|
1265
|
+
by_side_links: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
1266
|
+
for record, side_links in to_side_links.items():
|
|
1267
|
+
for side_link in side_links:
|
|
1268
|
+
by_side_links.setdefault(side_link, []).append(record)
|
|
1269
|
+
return by_side_links
|
|
1263
1270
|
|
|
1264
1271
|
# FR-46155: Update relationship path traversing functions to be non-static and take in a wrapper type so that the
|
|
1265
1272
|
# output can be wrapped instead of requiring the user to wrap the output.
|
|
@@ -1362,7 +1369,9 @@ class RecordHandler:
|
|
|
1362
1369
|
elif direction == RelationshipNodeType.DESCENDANT:
|
|
1363
1370
|
next_search.update(self.an_man.get_descendant_of_type(search, data_type))
|
|
1364
1371
|
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
1365
|
-
|
|
1372
|
+
side_link: RecordModel | None = search.get_forward_side_link(node.data_field_name)
|
|
1373
|
+
if side_link:
|
|
1374
|
+
next_search.add(side_link)
|
|
1366
1375
|
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
1367
1376
|
next_search.update(search.get_reverse_side_link(data_type, node.data_field_name))
|
|
1368
1377
|
else:
|
|
@@ -1412,7 +1421,8 @@ class RecordHandler:
|
|
|
1412
1421
|
elif direction == RelationshipNodeType.DESCENDANT:
|
|
1413
1422
|
current = list(self.an_man.get_descendant_of_type(current[0], data_type))
|
|
1414
1423
|
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
1415
|
-
|
|
1424
|
+
side_link: RecordModel | None = current[0].get_forward_side_link(node.data_field_name)
|
|
1425
|
+
current = [side_link] if side_link else []
|
|
1416
1426
|
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
1417
1427
|
current = current[0].get_reverse_side_link(data_type, node.data_field_name)
|
|
1418
1428
|
else:
|