sapiopycommons 2025.6.19a564__py3-none-any.whl → 2026.1.22a847__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.
Files changed (63) hide show
  1. sapiopycommons/ai/__init__.py +0 -0
  2. sapiopycommons/ai/agent_service_base.py +2051 -0
  3. sapiopycommons/ai/converter_service_base.py +163 -0
  4. sapiopycommons/ai/external_credentials.py +131 -0
  5. sapiopycommons/ai/protoapi/agent/agent_pb2.py +87 -0
  6. sapiopycommons/ai/protoapi/agent/agent_pb2.pyi +282 -0
  7. sapiopycommons/ai/protoapi/agent/agent_pb2_grpc.py +154 -0
  8. sapiopycommons/ai/protoapi/agent/entry_pb2.py +49 -0
  9. sapiopycommons/ai/protoapi/agent/entry_pb2.pyi +40 -0
  10. sapiopycommons/ai/protoapi/agent/entry_pb2_grpc.py +24 -0
  11. sapiopycommons/ai/protoapi/agent/item/item_container_pb2.py +61 -0
  12. sapiopycommons/ai/protoapi/agent/item/item_container_pb2.pyi +181 -0
  13. sapiopycommons/ai/protoapi/agent/item/item_container_pb2_grpc.py +24 -0
  14. sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2.py +41 -0
  15. sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2.pyi +36 -0
  16. sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2_grpc.py +24 -0
  17. sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.py +51 -0
  18. sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.pyi +59 -0
  19. sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2_grpc.py +24 -0
  20. sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.py +123 -0
  21. sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.pyi +599 -0
  22. sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2_grpc.py +24 -0
  23. sapiopycommons/ai/protoapi/pipeline/converter/converter_pb2.py +59 -0
  24. sapiopycommons/ai/protoapi/pipeline/converter/converter_pb2.pyi +68 -0
  25. sapiopycommons/ai/protoapi/pipeline/converter/converter_pb2_grpc.py +149 -0
  26. sapiopycommons/ai/protoapi/pipeline/script/script_pb2.py +69 -0
  27. sapiopycommons/ai/protoapi/pipeline/script/script_pb2.pyi +109 -0
  28. sapiopycommons/ai/protoapi/pipeline/script/script_pb2_grpc.py +153 -0
  29. sapiopycommons/ai/protoapi/pipeline/step_output_pb2.py +49 -0
  30. sapiopycommons/ai/protoapi/pipeline/step_output_pb2.pyi +56 -0
  31. sapiopycommons/ai/protoapi/pipeline/step_output_pb2_grpc.py +24 -0
  32. sapiopycommons/ai/protoapi/pipeline/step_pb2.py +43 -0
  33. sapiopycommons/ai/protoapi/pipeline/step_pb2.pyi +44 -0
  34. sapiopycommons/ai/protoapi/pipeline/step_pb2_grpc.py +24 -0
  35. sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.py +39 -0
  36. sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.pyi +33 -0
  37. sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2_grpc.py +24 -0
  38. sapiopycommons/ai/protobuf_utils.py +583 -0
  39. sapiopycommons/ai/request_validation.py +561 -0
  40. sapiopycommons/ai/server.py +152 -0
  41. sapiopycommons/ai/test_client.py +534 -0
  42. sapiopycommons/callbacks/callback_util.py +53 -24
  43. sapiopycommons/eln/experiment_handler.py +12 -5
  44. sapiopycommons/files/assay_plate_reader.py +93 -0
  45. sapiopycommons/files/file_text_converter.py +207 -0
  46. sapiopycommons/files/file_util.py +128 -1
  47. sapiopycommons/files/temp_files.py +82 -0
  48. sapiopycommons/flowcyto/flow_cyto.py +2 -24
  49. sapiopycommons/general/accession_service.py +2 -28
  50. sapiopycommons/general/aliases.py +4 -1
  51. sapiopycommons/general/macros.py +172 -0
  52. sapiopycommons/general/time_util.py +199 -4
  53. sapiopycommons/multimodal/multimodal.py +2 -24
  54. sapiopycommons/recordmodel/record_handler.py +200 -111
  55. sapiopycommons/rules/eln_rule_handler.py +3 -0
  56. sapiopycommons/rules/on_save_rule_handler.py +3 -0
  57. sapiopycommons/webhook/webhook_handlers.py +6 -4
  58. sapiopycommons/webhook/webservice_handlers.py +1 -1
  59. {sapiopycommons-2025.6.19a564.dist-info → sapiopycommons-2026.1.22a847.dist-info}/METADATA +2 -2
  60. sapiopycommons-2026.1.22a847.dist-info/RECORD +113 -0
  61. sapiopycommons-2025.6.19a564.dist-info/RECORD +0 -68
  62. {sapiopycommons-2025.6.19a564.dist-info → sapiopycommons-2026.1.22a847.dist-info}/WHEEL +0 -0
  63. {sapiopycommons-2025.6.19a564.dist-info → sapiopycommons-2026.1.22a847.dist-info}/licenses/LICENSE +0 -0
@@ -3,9 +3,13 @@ from __future__ import annotations
3
3
  import io
4
4
  import warnings
5
5
  from collections.abc import Iterable
6
- from typing import Collection
6
+ from typing import Collection, TypeVar
7
7
  from weakref import WeakValueDictionary
8
8
 
9
+ from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
10
+ FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey, DataTypeIdentifier
11
+ from sapiopycommons.general.custom_report_util import CustomReportUtil
12
+ from sapiopycommons.general.exceptions import SapioException
9
13
  from sapiopylib.rest.DataRecordManagerService import DataRecordManager
10
14
  from sapiopylib.rest.User import SapioUser
11
15
  from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, RawReportTerm, ReportColumn
@@ -24,19 +28,16 @@ from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, Wr
24
28
  from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
25
29
  RelationshipNodeType
26
30
  from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
27
- from sapiopylib.rest.utils.recordmodel.properties import Parents, Parent, Children, Child, ForwardSideLink
31
+ from sapiopylib.rest.utils.recordmodel.properties import Parents, Parent, Children, Child, ForwardSideLink, \
32
+ ReverseSideLink
28
33
 
29
- from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
30
- FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey, DataTypeIdentifier
31
- from sapiopycommons.general.custom_report_util import CustomReportUtil
32
- from sapiopycommons.general.exceptions import SapioException
34
+ # CR-47717: Use TypeVars in the type hints of certain functions to prevent PyCharm from erroneously flagging certain
35
+ # return type hints as incorrect.
36
+ IsRecordModel = TypeVar('IsRecordModel', bound=RecordModel)
37
+ """A PyRecordModel or AbstractRecordModel."""
38
+ IsSapioRecord = TypeVar('IsSapioRecord', bound=SapioRecord)
39
+ """A DataRecord, PyRecordModel, or AbstractRecordModel."""
33
40
 
34
- # Aliases for longer name.
35
- _PropertyGetter = AbstractRecordModelPropertyGetter
36
- _PropertyAdder = AbstractRecordModelPropertyAdder
37
- _PropertyRemover = AbstractRecordModelPropertyRemover
38
- _PropertySetter = AbstractRecordModelPropertySetter
39
- _PropertyType = RecordModelPropertyType
40
41
 
41
42
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
42
43
  # FR-47575 - Reordered functions so that the Java and Python versions are as close to each other as possible.
@@ -92,6 +93,10 @@ class RecordHandler:
92
93
  PyRecordModel instead of a WrappedRecordModel.
93
94
  :return: The record model for the input.
94
95
  """
96
+ # PR-47792: Set the wrapper_type to None if a str was provided instead of a type[WrappedType]. The type hints
97
+ # say this shouldn't be done anyway, but using this as a safeguard against user error.
98
+ if isinstance(wrapper_type, str):
99
+ wrapper_type = None
95
100
  if wrapper_type is not None:
96
101
  self.__verify_data_type(record, wrapper_type)
97
102
  if isinstance(record, PyRecordModel):
@@ -524,9 +529,11 @@ class RecordHandler:
524
529
  """
525
530
  warnings.warn("Deprecated in favor of the [System/Custom/Quick]ReportRecordAutoPager classes.", DeprecationWarning)
526
531
  if isinstance(report_name, str):
532
+ # noinspection PyDeprecation
527
533
  results: list[dict[str, FieldValue]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
528
534
  page_limit, page_size, page_number)
529
535
  elif isinstance(report_name, RawReportTerm):
536
+ # noinspection PyDeprecation
530
537
  results: list[dict[str, FieldValue]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
531
538
  page_limit, page_size, page_number)
532
539
  elif isinstance(report_name, CustomReportCriteria):
@@ -539,6 +546,7 @@ class RecordHandler:
539
546
  # Enforce that the given custom report has a record ID column.
540
547
  if not any([x.data_type_name == dt and x.data_field_name == "RecordId" for x in report_name.column_list]):
541
548
  report_name.column_list.append(ReportColumn(dt, "RecordId", FieldType.LONG))
549
+ # noinspection PyDeprecation
542
550
  results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, report_name, filters,
543
551
  page_limit, page_size, page_number)
544
552
  else:
@@ -551,7 +559,7 @@ class RecordHandler:
551
559
  return self.query_models_by_id(wrapper_type, ids)
552
560
 
553
561
  @staticmethod
554
- def map_by_id(models: Iterable[SapioRecord]) -> dict[int, SapioRecord]:
562
+ def map_by_id(models: Iterable[IsSapioRecord]) -> dict[int, IsSapioRecord]:
555
563
  """
556
564
  Map the given records their record IDs.
557
565
 
@@ -560,12 +568,12 @@ class RecordHandler:
560
568
  """
561
569
  ret_dict: dict[int, SapioRecord] = {}
562
570
  for model in models:
563
- ret_dict.update({model.record_id: model})
571
+ ret_dict.update({AliasUtil.to_record_id(model): model})
564
572
  return ret_dict
565
573
 
566
574
  @staticmethod
567
- def map_by_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
568
- -> dict[FieldValue, list[SapioRecord]]:
575
+ def map_by_field(models: Iterable[IsSapioRecord], field_name: FieldIdentifier) \
576
+ -> dict[FieldValue, list[IsSapioRecord]]:
569
577
  """
570
578
  Map the given records by one of their fields. If any two records share the same field value, they'll appear in
571
579
  the same value list.
@@ -582,8 +590,8 @@ class RecordHandler:
582
590
  return ret_dict
583
591
 
584
592
  @staticmethod
585
- def map_by_unique_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
586
- -> dict[FieldValue, SapioRecord]:
593
+ def map_by_unique_field(models: Iterable[IsSapioRecord], field_name: FieldIdentifier) \
594
+ -> dict[FieldValue, IsSapioRecord]:
587
595
  """
588
596
  Uniquely map the given records by one of their fields. If any two records share the same field value, throws
589
597
  an exception.
@@ -631,6 +639,40 @@ class RecordHandler:
631
639
  with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as stream:
632
640
  self.dr_man.set_record_image(record, stream)
633
641
 
642
+ def get_file_blob_data(self, record: SapioRecord, field_name: FieldIdentifier) -> bytes:
643
+ """
644
+ Retrieve file blob data for a given record from one of its file blob fields.
645
+
646
+ :param record: The record model to retrieve from.
647
+ :param field_name: The name of the file blob field to retrieve the data from.
648
+ :return: The file bytes of the given record's file blob data for the input field.
649
+ """
650
+ record: DataRecord = AliasUtil.to_data_record(record)
651
+ field_name: str = AliasUtil.to_data_field_name(field_name)
652
+ with io.BytesIO() as data_sink:
653
+ def consume_data(chunk: bytes):
654
+ data_sink.write(chunk)
655
+
656
+ self.dr_man.get_file_blob_data(record, field_name, consume_data)
657
+ data_sink.flush()
658
+ data_sink.seek(0)
659
+ file_bytes = data_sink.read()
660
+ return file_bytes
661
+
662
+ def set_file_blob_data(self, record: SapioRecord, field_name: FieldIdentifier, file_name: str, file_data: str | bytes) -> None:
663
+ """
664
+ Set the file blob data for a given record on one of its file blob fields.
665
+
666
+ :param record: The record model to set the file blob data of.
667
+ :param field_name: The name of the file blob field to set the data for.
668
+ :param file_name: The name of the file being stored in the file blob field.
669
+ :param file_data: The file data of the blob to set on the record.
670
+ """
671
+ record: DataRecord = AliasUtil.to_data_record(record)
672
+ field_name: str = AliasUtil.to_data_field_name(field_name)
673
+ with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as stream:
674
+ self.dr_man.set_file_blob_data(record, field_name, file_name, stream)
675
+
634
676
  @staticmethod
635
677
  def sum_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
636
678
  """
@@ -662,7 +704,7 @@ class RecordHandler:
662
704
  return RecordHandler.sum_of_field(models, field_name) / len(models)
663
705
 
664
706
  @staticmethod
665
- def get_newest_record(records: Iterable[SapioRecord]) -> SapioRecord:
707
+ def get_newest_record(records: Iterable[IsSapioRecord]) -> IsSapioRecord:
666
708
  """
667
709
  Get the newest record from a list of records.
668
710
 
@@ -673,7 +715,7 @@ class RecordHandler:
673
715
 
674
716
  # FR-46696: Add a function for getting the oldest record in a list, just like we have one for the newest record.
675
717
  @staticmethod
676
- def get_oldest_record(records: Iterable[SapioRecord]) -> SapioRecord:
718
+ def get_oldest_record(records: Iterable[IsSapioRecord]) -> IsSapioRecord:
677
719
  """
678
720
  Get the oldest record from a list of records.
679
721
 
@@ -683,7 +725,7 @@ class RecordHandler:
683
725
  return min(records, key=lambda x: x.record_id)
684
726
 
685
727
  @staticmethod
686
- def get_min_record(records: list[RecordModel], field: FieldIdentifier) -> RecordModel:
728
+ def get_min_record(records: list[IsSapioRecord], field: FieldIdentifier) -> IsSapioRecord:
687
729
  """
688
730
  Get the record model with the minimum value of a given field from a list of record models.
689
731
 
@@ -695,7 +737,7 @@ class RecordHandler:
695
737
  return min(records, key=lambda x: x.get_field_value(field))
696
738
 
697
739
  @staticmethod
698
- def get_max_record(records: list[RecordModel], field: FieldIdentifier) -> RecordModel:
740
+ def get_max_record(records: list[IsSapioRecord], field: FieldIdentifier) -> IsSapioRecord:
699
741
  """
700
742
  Get the record model with the maximum value of a given field from a list of record models.
701
743
 
@@ -772,7 +814,8 @@ class RecordHandler:
772
814
  return [{field_name: value} for value in values]
773
815
 
774
816
  @staticmethod
775
- def get_from_all(records: Iterable[RecordModel], getter: _PropertyGetter[_PropertyType]) \
817
+ def get_from_all(records: Iterable[RecordModel],
818
+ getter: AbstractRecordModelPropertyGetter[RecordModelPropertyType]) \
776
819
  -> list[RecordModelPropertyType]:
777
820
  """
778
821
  Use a getter property on all records in a list of record models. For example, you can iterate over a list of
@@ -787,7 +830,8 @@ class RecordHandler:
787
830
  return [x.get(getter) for x in records]
788
831
 
789
832
  @staticmethod
790
- def set_on_all(records: Iterable[RecordModel], setter: _PropertySetter[_PropertyType]) \
833
+ def set_on_all(records: Iterable[RecordModel],
834
+ setter: AbstractRecordModelPropertySetter[RecordModelPropertyType]) \
791
835
  -> list[RecordModelPropertyType]:
792
836
  """
793
837
  Use a setter property on all records in a list of record models. For example, you can iterate over a list of
@@ -802,7 +846,8 @@ class RecordHandler:
802
846
  return [x.set(setter) for x in records]
803
847
 
804
848
  @staticmethod
805
- def add_to_all(records: Iterable[RecordModel], adder: _PropertyAdder[_PropertyType]) \
849
+ def add_to_all(records: Iterable[RecordModel],
850
+ adder: AbstractRecordModelPropertyAdder[RecordModelPropertyType]) \
806
851
  -> list[RecordModelPropertyType]:
807
852
  """
808
853
  Use an adder property on all records in a list of record models. For example, you can iterate over a list of
@@ -816,7 +861,8 @@ class RecordHandler:
816
861
  return [x.add(adder) for x in records]
817
862
 
818
863
  @staticmethod
819
- def remove_from_all(records: Iterable[RecordModel], remover: _PropertyRemover[_PropertyType]) \
864
+ def remove_from_all(records: Iterable[RecordModel],
865
+ remover: AbstractRecordModelPropertyRemover[RecordModelPropertyType]) \
820
866
  -> list[RecordModelPropertyType]:
821
867
  """
822
868
  Use a remover property on all records in a list of record models. For example, you can iterate over a list of
@@ -870,7 +916,7 @@ class RecordHandler:
870
916
  parent_dt: str = AliasUtil.to_data_type_name(parent_type)
871
917
  wrapper: type[WrappedType] | None = parent_type if isinstance(parent_type, type) else None
872
918
  record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
873
- parent: PyRecordModel | None = record.get_parent_of_type(parent_dt)
919
+ parent: PyRecordModel | None = record.get(Parent.of_type_name(parent_dt))
874
920
  if parent is not None:
875
921
  return self.wrap_model(parent, wrapper) if wrapper else parent
876
922
  return record.add(Parent.create(wrapper)) if wrapper else record.add(Parent.create_by_name(parent_dt))
@@ -888,7 +934,7 @@ class RecordHandler:
888
934
  child_dt: str = AliasUtil.to_data_type_name(child_type)
889
935
  wrapper: type[WrappedType] | None = child_type if isinstance(child_type, type) else None
890
936
  record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
891
- child: PyRecordModel | None = record.get_child_of_type(child_dt)
937
+ child: PyRecordModel | None = record.get(Child.of_type_name(child_dt))
892
938
  if child is not None:
893
939
  return self.wrap_model(child, wrapper) if wrapper else child
894
940
  return record.add(Child.create(wrapper)) if wrapper else record.add(Child.create_by_name(child_dt))
@@ -908,7 +954,7 @@ class RecordHandler:
908
954
  side_link_field: str = AliasUtil.to_data_field_name(side_link_field)
909
955
  wrapper: type[WrappedType] | None = side_link_type if isinstance(side_link_type, type) else None
910
956
  record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
911
- side_link: PyRecordModel | None = record.get_forward_side_link(side_link_field)
957
+ side_link: PyRecordModel | None = record.get(ForwardSideLink.of(side_link_field))
912
958
  if side_link is not None:
913
959
  return self.wrap_model(side_link, wrapper) if wrapper else side_link
914
960
  side_link: WrappedType | PyRecordModel = self.add_model(side_link_type)
@@ -955,52 +1001,63 @@ class RecordHandler:
955
1001
  if child not in children:
956
1002
  record.remove(Child.ref(child))
957
1003
 
1004
+ # CR-47717: Update the map_[to/by]_[relationship] functions to allow PyRecordModels to be provided and returned
1005
+ # instead of only using WrappedRecordModels and wrapper types.
958
1006
  @staticmethod
959
- def map_to_parent(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType])\
960
- -> dict[WrappedRecordModel, WrappedType]:
1007
+ def map_to_parent(models: Iterable[IsRecordModel], parent_type: type[WrappedType] | str) \
1008
+ -> dict[IsRecordModel, WrappedType | PyRecordModel]:
961
1009
  """
962
1010
  Map a list of record models to a single parent of a given type. The parents must already be loaded.
963
1011
 
964
1012
  :param models: A list of record models.
965
- :param parent_type: The record model wrapper of the parent.
1013
+ :param parent_type: The record model wrapper or data type name of the parents. If a data type name is
1014
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
966
1015
  :return: A dict[ModelType, ParentType]. If an input model doesn't have a parent of the given parent type, then
967
1016
  it will map to None.
968
1017
  """
969
- return_dict: dict[WrappedRecordModel, WrappedType] = {}
1018
+ return_dict: dict[RecordModel, WrappedType | PyRecordModel] = {}
970
1019
  for model in models:
971
- return_dict[model] = model.get_parent_of_type(parent_type)
1020
+ if isinstance(parent_type, str):
1021
+ return_dict[model] = model.get(Parent.of_type_name(parent_type))
1022
+ else:
1023
+ return_dict[model] = model.get(Parent.of_type(parent_type))
972
1024
  return return_dict
973
1025
 
974
1026
  @staticmethod
975
- def map_to_parents(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
976
- -> dict[WrappedRecordModel, list[WrappedType]]:
1027
+ def map_to_parents(models: Iterable[IsRecordModel], parent_type: type[WrappedType] | str) \
1028
+ -> dict[IsRecordModel, list[WrappedType] | list[PyRecordModel]]:
977
1029
  """
978
1030
  Map a list of record models to a list parents of a given type. The parents must already be loaded.
979
1031
 
980
1032
  :param models: A list of record models.
981
- :param parent_type: The record model wrapper of the parents.
1033
+ :param parent_type: The record model wrapper or data type name of the parents. If a data type name is
1034
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
982
1035
  :return: A dict[ModelType, list[ParentType]]. If an input model doesn't have a parent of the given parent type,
983
1036
  then it will map to an empty list.
984
1037
  """
985
- return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
1038
+ return_dict: dict[WrappedRecordModel, list[WrappedType] | list[PyRecordModel]] = {}
986
1039
  for model in models:
987
- return_dict[model] = model.get_parents_of_type(parent_type)
1040
+ if isinstance(parent_type, str):
1041
+ return_dict[model] = model.get(Parents.of_type_name(parent_type))
1042
+ else:
1043
+ return_dict[model] = model.get(Parents.of_type(parent_type))
988
1044
  return return_dict
989
1045
 
990
1046
  @staticmethod
991
- def map_by_parent(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
992
- -> dict[WrappedType, WrappedRecordModel]:
1047
+ def map_by_parent(models: Iterable[IsRecordModel], parent_type: type[WrappedType] | str) \
1048
+ -> dict[WrappedType | PyRecordModel, IsRecordModel]:
993
1049
  """
994
1050
  Take a list of record models and map them by their parent. Essentially an inversion of map_to_parent.
995
1051
  If two records share the same parent, an exception will be thrown. The parents must already be loaded.
996
1052
 
997
1053
  :param models: A list of record models.
998
- :param parent_type: The record model wrapper of the parents.
1054
+ :param parent_type: The record model wrapper or data type name of the parents. If a data type name is
1055
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
999
1056
  :return: A dict[ParentType, ModelType]. If an input model doesn't have a parent of the given parent type,
1000
1057
  then it will not be in the resulting dictionary.
1001
1058
  """
1002
- to_parent: dict[WrappedRecordModel, WrappedType] = RecordHandler.map_to_parent(models, parent_type)
1003
- by_parent: dict[WrappedType, WrappedRecordModel] = {}
1059
+ to_parent: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler.map_to_parent(models, parent_type)
1060
+ by_parent: dict[WrappedType | PyRecordModel, RecordModel] = {}
1004
1061
  for record, parent in to_parent.items():
1005
1062
  if parent is None:
1006
1063
  continue
@@ -1011,70 +1068,81 @@ class RecordHandler:
1011
1068
  return by_parent
1012
1069
 
1013
1070
  @staticmethod
1014
- def map_by_parents(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
1015
- -> dict[WrappedType, list[WrappedRecordModel]]:
1071
+ def map_by_parents(models: Iterable[IsRecordModel], parent_type: type[WrappedType] | str) \
1072
+ -> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
1016
1073
  """
1017
1074
  Take a list of record models and map them by their parents. Essentially an inversion of map_to_parents. Input
1018
1075
  models that share a parent will end up in the same list. The parents must already be loaded.
1019
1076
 
1020
1077
  :param models: A list of record models.
1021
- :param parent_type: The record model wrapper of the parents.
1078
+ :param parent_type: The record model wrapper or data type name of the parents. If a data type name is
1079
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1022
1080
  :return: A dict[ParentType, list[ModelType]]. If an input model doesn't have a parent of the given parent type,
1023
1081
  then it will not be in the resulting dictionary.
1024
1082
  """
1025
- to_parents: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler.map_to_parents(models, parent_type)
1026
- by_parents: dict[WrappedType, list[WrappedRecordModel]] = {}
1083
+ to_parents: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = RecordHandler\
1084
+ .map_to_parents(models, parent_type)
1085
+ by_parents: dict[WrappedType | PyRecordModel, list[RecordModel]] = {}
1027
1086
  for record, parents in to_parents.items():
1028
1087
  for parent in parents:
1029
1088
  by_parents.setdefault(parent, []).append(record)
1030
1089
  return by_parents
1031
1090
 
1032
1091
  @staticmethod
1033
- def map_to_child(models: Iterable[WrappedRecordModel], child_type: type[WrappedType])\
1034
- -> dict[WrappedRecordModel, WrappedType]:
1092
+ def map_to_child(models: Iterable[IsRecordModel], child_type: type[WrappedType] | str) \
1093
+ -> dict[IsRecordModel, WrappedType | PyRecordModel]:
1035
1094
  """
1036
1095
  Map a list of record models to a single child of a given type. The children must already be loaded.
1037
1096
 
1038
1097
  :param models: A list of record models.
1039
- :param child_type: The record model wrapper of the child.
1098
+ :param child_type: The record model wrapper or data type name of the children. If a data type name is
1099
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1040
1100
  :return: A dict[ModelType, ChildType]. If an input model doesn't have a child of the given child type, then
1041
1101
  it will map to None.
1042
1102
  """
1043
- return_dict: dict[WrappedRecordModel, WrappedType] = {}
1103
+ return_dict: dict[RecordModel, WrappedType | PyRecordModel] = {}
1044
1104
  for model in models:
1045
- return_dict[model] = model.get_child_of_type(child_type)
1105
+ if isinstance(child_type, str):
1106
+ return_dict[model] = model.get(Child.of_type_name(child_type))
1107
+ else:
1108
+ return_dict[model] = model.get(Child.of_type(child_type))
1046
1109
  return return_dict
1047
1110
 
1048
1111
  @staticmethod
1049
- def map_to_children(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
1050
- -> dict[WrappedRecordModel, list[WrappedType]]:
1112
+ def map_to_children(models: Iterable[IsRecordModel], child_type: type[WrappedType] | str) \
1113
+ -> dict[IsRecordModel, list[WrappedType] | PyRecordModel]:
1051
1114
  """
1052
1115
  Map a list of record models to a list children of a given type. The children must already be loaded.
1053
1116
 
1054
1117
  :param models: A list of record models.
1055
- :param child_type: The record model wrapper of the children.
1118
+ :param child_type: The record model wrapper or data type name of the children. If a data type name is
1119
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1056
1120
  :return: A dict[ModelType, list[ChildType]]. If an input model doesn't have children of the given child type,
1057
1121
  then it will map to an empty list.
1058
1122
  """
1059
- return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
1123
+ return_dict: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = {}
1060
1124
  for model in models:
1061
- return_dict[model] = model.get_children_of_type(child_type)
1125
+ if isinstance(child_type, str):
1126
+ return_dict[model] = model.get(Children.of_type_name(child_type))
1127
+ else:
1128
+ return_dict[model] = model.get(Children.of_type(child_type))
1062
1129
  return return_dict
1063
1130
 
1064
1131
  @staticmethod
1065
- def map_by_child(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
1066
- -> dict[WrappedType, WrappedRecordModel]:
1132
+ def map_by_child(models: Iterable[IsRecordModel], child_type: type[WrappedType] | str) \
1133
+ -> dict[WrappedType | str, IsRecordModel]:
1067
1134
  """
1068
1135
  Take a list of record models and map them by their children. Essentially an inversion of map_to_child.
1069
1136
  If two records share the same child, an exception will be thrown. The children must already be loaded.
1070
1137
 
1071
1138
  :param models: A list of record models.
1072
- :param child_type: The record model wrapper of the children.
1139
+ :param child_type: The record model wrapper or data type name of the children. If a data type name is
1140
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1073
1141
  :return: A dict[ChildType, ModelType]. If an input model doesn't have a child of the given child type,
1074
1142
  then it will not be in the resulting dictionary.
1075
1143
  """
1076
- to_child: dict[WrappedRecordModel, WrappedType] = RecordHandler.map_to_child(models, child_type)
1077
- by_child: dict[WrappedType, WrappedRecordModel] = {}
1144
+ to_child: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler.map_to_child(models, child_type)
1145
+ by_child: dict[WrappedType | PyRecordModel, RecordModel] = {}
1078
1146
  for record, child in to_child.items():
1079
1147
  if child is None:
1080
1148
  continue
@@ -1085,45 +1153,50 @@ class RecordHandler:
1085
1153
  return by_child
1086
1154
 
1087
1155
  @staticmethod
1088
- def map_by_children(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
1089
- -> dict[WrappedType, list[WrappedRecordModel]]:
1156
+ def map_by_children(models: Iterable[IsRecordModel], child_type: type[WrappedType] | str) \
1157
+ -> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
1090
1158
  """
1091
1159
  Take a list of record models and map them by their children. Essentially an inversion of map_to_children. Input
1092
1160
  models that share a child will end up in the same list. The children must already be loaded.
1093
1161
 
1094
1162
  :param models: A list of record models.
1095
- :param child_type: The record model wrapper of the children.
1163
+ :param child_type: The record model wrapper or data type name of the children. If a data type name is
1164
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1096
1165
  :return: A dict[ChildType, list[ModelType]]. If an input model doesn't have children of the given child type,
1097
1166
  then it will not be in the resulting dictionary.
1098
1167
  """
1099
- to_children: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler.map_to_children(models, child_type)
1100
- by_children: dict[WrappedType, list[WrappedRecordModel]] = {}
1168
+ to_children: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = RecordHandler\
1169
+ .map_to_children(models, child_type)
1170
+ by_children: dict[WrappedType | PyRecordModel, list[RecordModel]] = {}
1101
1171
  for record, children in to_children.items():
1102
1172
  for child in children:
1103
1173
  by_children.setdefault(child, []).append(record)
1104
1174
  return by_children
1105
1175
 
1106
1176
  @staticmethod
1107
- def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1108
- side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
1177
+ def map_to_forward_side_link(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1178
+ side_link_type: type[WrappedType] | None) \
1179
+ -> dict[IsRecordModel, WrappedType | PyRecordModel]:
1109
1180
  """
1110
1181
  Map a list of record models to their forward side link. The forward side link must already be loaded.
1111
1182
 
1112
1183
  :param models: A list of record models.
1113
1184
  :param field_name: The field name on the record models where the side link is located.
1114
- :param side_link_type: The record model wrapper of the forward side link.
1185
+ :param side_link_type: The record model wrapper of the forward side link. If None, the side links will
1186
+ be returned as PyRecordModels instead of WrappedRecordModels.
1115
1187
  :return: A dict[ModelType, SlideLink]. If an input model doesn't have a forward side link of the given type,
1116
1188
  then it will map to None.
1117
1189
  """
1118
1190
  field_name: str = AliasUtil.to_data_field_name(field_name)
1119
- return_dict: dict[WrappedRecordModel, WrappedType] = {}
1191
+ return_dict: dict[RecordModel, WrappedType | PyRecordModel] = {}
1120
1192
  for model in models:
1121
- return_dict[model] = model.get_forward_side_link(field_name, side_link_type)
1193
+ return_dict[model] = model.get(ForwardSideLink.of(field_name, side_link_type))
1122
1194
  return return_dict
1123
1195
 
1124
1196
  @staticmethod
1125
- def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1126
- side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
1197
+ def map_by_forward_side_link(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1198
+ side_link_type: type[WrappedType] | None) \
1199
+ -> dict[WrappedType | PyRecordModel, IsRecordModel]:
1127
1200
  """
1128
1201
  Take a list of record models and map them by their forward side link. Essentially an inversion of
1129
1202
  map_to_forward_side_link, but if two records share the same forward link, an exception is thrown.
@@ -1131,14 +1204,15 @@ class RecordHandler:
1131
1204
 
1132
1205
  :param models: A list of record models.
1133
1206
  :param field_name: The field name on the record models where the side link is located.
1134
- :param side_link_type: The record model wrapper of the forward side links.
1207
+ :param side_link_type: The record model wrapper of the forward side links. If None, the side links will
1208
+ be returned as PyRecordModels instead of WrappedRecordModels.
1135
1209
  :return: A dict[SideLink, ModelType]. If an input model doesn't have a forward side link of the given type
1136
1210
  pointing to it, then it will not be in the resulting dictionary.
1137
1211
  """
1138
1212
  field_name: str = AliasUtil.to_data_field_name(field_name)
1139
- to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
1213
+ to_side_link: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler\
1140
1214
  .map_to_forward_side_link(models, field_name, side_link_type)
1141
- by_side_link: dict[WrappedType, WrappedRecordModel] = {}
1215
+ by_side_link: dict[WrappedType | PyRecordModel, RecordModel] = {}
1142
1216
  for record, side_link in to_side_link.items():
1143
1217
  if side_link is None:
1144
1218
  continue
@@ -1149,8 +1223,9 @@ class RecordHandler:
1149
1223
  return by_side_link
1150
1224
 
1151
1225
  @staticmethod
1152
- def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1153
- side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
1226
+ def map_by_forward_side_links(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1227
+ side_link_type: type[WrappedType] | None) \
1228
+ -> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
1154
1229
  """
1155
1230
  Take a list of record models and map them by their forward side link. Essentially an inversion of
1156
1231
  map_to_forward_side_link. Input models that share a forward side link will end up in the same list.
@@ -1158,14 +1233,15 @@ class RecordHandler:
1158
1233
 
1159
1234
  :param models: A list of record models.
1160
1235
  :param field_name: The field name on the record models where the side link is located.
1161
- :param side_link_type: The record model wrapper of the forward side links.
1236
+ :param side_link_type: The record model wrapper of the forward side links. If None, the side links will
1237
+ be returned as PyRecordModels instead of WrappedRecordModels.
1162
1238
  :return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
1163
1239
  pointing to it, then it will not be in the resulting dictionary.
1164
1240
  """
1165
1241
  field_name: str = AliasUtil.to_data_field_name(field_name)
1166
- to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
1242
+ to_side_link: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler\
1167
1243
  .map_to_forward_side_link(models, field_name, side_link_type)
1168
- by_side_link: dict[WrappedType, list[WrappedRecordModel]] = {}
1244
+ by_side_link: dict[WrappedType | PyRecordModel, list[RecordModel]] = {}
1169
1245
  for record, side_link in to_side_link.items():
1170
1246
  if side_link is None:
1171
1247
  continue
@@ -1173,8 +1249,9 @@ class RecordHandler:
1173
1249
  return by_side_link
1174
1250
 
1175
1251
  @staticmethod
1176
- def map_to_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1177
- side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
1252
+ def map_to_reverse_side_link(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1253
+ side_link_type: type[WrappedType] | str) \
1254
+ -> dict[IsRecordModel, WrappedType | PyRecordModel]:
1178
1255
  """
1179
1256
  Map a list of record models to the reverse side link of a given type. If a given record has more than one
1180
1257
  reverse side link of this type, an exception is thrown. The reverse side links must already be loaded.
@@ -1182,14 +1259,18 @@ class RecordHandler:
1182
1259
  :param models: A list of record models.
1183
1260
  :param field_name: The field name on the side linked model where the side link to the given record models is
1184
1261
  located.
1185
- :param side_link_type: The record model wrapper of the reverse side links.
1262
+ :param side_link_type: The record model wrapper or data type name of the reverse side links. If a data type
1263
+ name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1186
1264
  :return: A dict[ModelType, SideLink]. If an input model doesn't have reverse side links of the given type,
1187
1265
  then it will map to None.
1188
1266
  """
1189
1267
  field_name: str = AliasUtil.to_data_field_name(field_name)
1190
- return_dict: dict[WrappedRecordModel, WrappedType] = {}
1268
+ return_dict: dict[RecordModel, WrappedType | PyRecordModel] = {}
1191
1269
  for model in models:
1192
- links: list[WrappedType] = model.get_reverse_side_link(field_name, side_link_type)
1270
+ if isinstance(side_link_type, str):
1271
+ links: list[WrappedType] = model.get(ReverseSideLink.of(side_link_type, field_name))
1272
+ else:
1273
+ links: list[WrappedType] = model.get(ReverseSideLink.of_type(side_link_type, field_name))
1193
1274
  if len(links) > 1:
1194
1275
  raise SapioException(f"Model {model.data_type_name} {model.record_id} has more than one reverse link "
1195
1276
  f"of type {side_link_type.get_wrapper_data_type_name()}.")
@@ -1197,8 +1278,9 @@ class RecordHandler:
1197
1278
  return return_dict
1198
1279
 
1199
1280
  @staticmethod
1200
- def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1201
- side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, list[WrappedType]]:
1281
+ def map_to_reverse_side_links(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1282
+ side_link_type: type[WrappedType] | str) \
1283
+ -> dict[IsRecordModel, list[WrappedType] | list[PyRecordModel]]:
1202
1284
  """
1203
1285
  Map a list of record models to a list reverse side links of a given type. The reverse side links must already
1204
1286
  be loaded.
@@ -1206,19 +1288,24 @@ class RecordHandler:
1206
1288
  :param models: A list of record models.
1207
1289
  :param field_name: The field name on the side linked model where the side link to the given record models is
1208
1290
  located.
1209
- :param side_link_type: The record model wrapper of the reverse side links.
1291
+ :param side_link_type: The record model wrapper or data type name of the reverse side links. If a data type
1292
+ name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1210
1293
  :return: A dict[ModelType, list[SideLink]]. If an input model doesn't have reverse side links of the given type,
1211
1294
  then it will map to an empty list.
1212
1295
  """
1213
1296
  field_name: str = AliasUtil.to_data_field_name(field_name)
1214
- return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
1297
+ return_dict: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = {}
1215
1298
  for model in models:
1216
- return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
1299
+ if isinstance(side_link_type, str):
1300
+ return_dict[model] = model.get(ReverseSideLink.of(side_link_type, field_name))
1301
+ else:
1302
+ return_dict[model] = model.get(ReverseSideLink.of_type(side_link_type, field_name))
1217
1303
  return return_dict
1218
1304
 
1219
1305
  @staticmethod
1220
- def map_by_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1221
- side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
1306
+ def map_by_reverse_side_link(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1307
+ side_link_type: type[WrappedType] | str) \
1308
+ -> dict[WrappedType | PyRecordModel, IsRecordModel]:
1222
1309
  """
1223
1310
  Take a list of record models and map them by their reverse side link. Essentially an inversion of
1224
1311
  map_to_reverse_side_link. If two records share the same reverse side link, an exception is thrown.
@@ -1227,14 +1314,15 @@ class RecordHandler:
1227
1314
  :param models: A list of record models.
1228
1315
  :param field_name: The field name on the side linked model where the side link to the given record models is
1229
1316
  located.
1230
- :param side_link_type: The record model wrapper of the reverse side links.
1317
+ :param side_link_type: The record model wrapper or data type name of the reverse side links. If a data type
1318
+ name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1231
1319
  :return: A dict[SideLink, ModelType]. If an input model doesn't have a reverse side link of the given type
1232
1320
  pointing to it, then it will not be in the resulting dictionary.
1233
1321
  """
1234
1322
  field_name: str = AliasUtil.to_data_field_name(field_name)
1235
- to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
1323
+ to_side_link: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler\
1236
1324
  .map_to_reverse_side_link(models, field_name, side_link_type)
1237
- by_side_link: dict[WrappedType, WrappedRecordModel] = {}
1325
+ by_side_link: dict[WrappedType | PyRecordModel, RecordModel] = {}
1238
1326
  for record, side_link in to_side_link.items():
1239
1327
  if side_link is None:
1240
1328
  continue
@@ -1245,8 +1333,8 @@ class RecordHandler:
1245
1333
  return by_side_link
1246
1334
 
1247
1335
  @staticmethod
1248
- def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1249
- side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
1336
+ def map_by_reverse_side_links(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1337
+ side_link_type: type[WrappedType] | str) -> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
1250
1338
  """
1251
1339
  Take a list of record models and map them by their reverse side links. Essentially an inversion of
1252
1340
  map_to_reverse_side_links. Input models that share a reverse side link will end up in the same list.
@@ -1255,7 +1343,8 @@ class RecordHandler:
1255
1343
  :param models: A list of record models.
1256
1344
  :param field_name: The field name on the side linked model where the side link to the given record models is
1257
1345
  located.
1258
- :param side_link_type: The record model wrapper of the reverse side links.
1346
+ :param side_link_type: The record model wrapper or data type name of the reverse side links. If a data type
1347
+ name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1259
1348
  :return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
1260
1349
  pointing to it, then it will not be in the resulting dictionary.
1261
1350
  """
@@ -1270,9 +1359,9 @@ class RecordHandler:
1270
1359
 
1271
1360
  # FR-46155: Update relationship path traversing functions to be non-static and take in a wrapper type so that the
1272
1361
  # output can be wrapped instead of requiring the user to wrap the output.
1273
- def get_linear_path(self, models: Iterable[RecordModel], path: RelationshipPath,
1362
+ def get_linear_path(self, models: Iterable[IsRecordModel], path: RelationshipPath,
1274
1363
  wrapper_type: type[WrappedType] | None = None) \
1275
- -> dict[RecordModel, WrappedType | PyRecordModel | None]:
1364
+ -> dict[IsRecordModel, WrappedType | PyRecordModel | None]:
1276
1365
  """
1277
1366
  Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
1278
1367
  path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
@@ -1285,7 +1374,7 @@ class RecordHandler:
1285
1374
  :return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
1286
1375
  path couldn't be reached, the record will map to None.
1287
1376
  """
1288
- ret_dict: dict[RecordModel, WrappedType | None] = {}
1377
+ ret_dict: dict[RecordModel, WrappedType | PyRecordModel | None] = {}
1289
1378
  # PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
1290
1379
  path: list[RelationshipNode] = path.path
1291
1380
  for model in models:
@@ -1332,9 +1421,9 @@ class RecordHandler:
1332
1421
  ret_dict.update({model: self.wrap_model(current, wrapper_type) if current else None})
1333
1422
  return ret_dict
1334
1423
 
1335
- def get_branching_path(self, models: Iterable[RecordModel], path: RelationshipPath,
1424
+ def get_branching_path(self, models: Iterable[IsRecordModel], path: RelationshipPath,
1336
1425
  wrapper_type: type[WrappedType] | None = None)\
1337
- -> dict[RecordModel, list[WrappedType] | list[PyRecordModel]]:
1426
+ -> dict[IsRecordModel, list[WrappedType] | list[PyRecordModel]]:
1338
1427
  """
1339
1428
  Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
1340
1429
  path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
@@ -1347,7 +1436,7 @@ class RecordHandler:
1347
1436
  :return: Each record model mapped to the records at the end of the path starting from itself. If the end of the
1348
1437
  path couldn't be reached, the record will map to an empty list.
1349
1438
  """
1350
- ret_dict: dict[RecordModel, list[WrappedType]] = {}
1439
+ ret_dict: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = {}
1351
1440
  # PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
1352
1441
  path: list[RelationshipNode] = path.path
1353
1442
  for model in models:
@@ -1383,9 +1472,9 @@ class RecordHandler:
1383
1472
 
1384
1473
  # FR-46155: Create a relationship traversing function that returns a single function at the end of the path like
1385
1474
  # get_linear_path but can handle branching paths in the middle of the search like get_branching_path.
1386
- def get_flat_path(self, models: Iterable[RecordModel], path: RelationshipPath,
1475
+ def get_flat_path(self, models: Iterable[IsRecordModel], path: RelationshipPath,
1387
1476
  wrapper_type: type[WrappedType] | None = None) \
1388
- -> dict[RecordModel, WrappedType | PyRecordModel | None]:
1477
+ -> dict[IsRecordModel, WrappedType | PyRecordModel | None]:
1389
1478
  """
1390
1479
  Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
1391
1480
  path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
@@ -1402,7 +1491,7 @@ class RecordHandler:
1402
1491
  :return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
1403
1492
  path couldn't be reached, the record will map to None.
1404
1493
  """
1405
- ret_dict: dict[RecordModel, WrappedType | None] = {}
1494
+ ret_dict: dict[RecordModel, WrappedType | PyRecordModel | None] = {}
1406
1495
  # PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
1407
1496
  path: list[RelationshipNode] = path.path
1408
1497
  for model in models: