sapiopycommons 2025.7.10a595__py3-none-any.whl → 2025.7.15a611__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.

@@ -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, TypeAlias
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,23 @@ 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
28
-
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
31
+ from sapiopylib.rest.utils.recordmodel.properties import Parents, Parent, Children, Child, ForwardSideLink, \
32
+ ReverseSideLink
33
33
 
34
34
  # Aliases for longer name.
35
- _PropertyGetter = AbstractRecordModelPropertyGetter
36
- _PropertyAdder = AbstractRecordModelPropertyAdder
37
- _PropertyRemover = AbstractRecordModelPropertyRemover
38
- _PropertySetter = AbstractRecordModelPropertySetter
39
- _PropertyType = RecordModelPropertyType
35
+ _PropertyGetter: TypeAlias = AbstractRecordModelPropertyGetter
36
+ _PropertyAdder: TypeAlias = AbstractRecordModelPropertyAdder
37
+ _PropertyRemover: TypeAlias = AbstractRecordModelPropertyRemover
38
+ _PropertySetter: TypeAlias = AbstractRecordModelPropertySetter
39
+ _PropertyType: TypeAlias = RecordModelPropertyType
40
+
41
+ # CR-47717: Use TypeVars in the type hints of certain functions to prevent PyCharm from erroneously flagging certain
42
+ # return type hints as incorrect.
43
+ IsRecordModel = TypeVar('IsRecordModel', bound=RecordModel)
44
+ """A PyRecordModel or AbstractRecordModel."""
45
+ IsSapioRecord = TypeVar('IsSapioRecord', bound=SapioRecord)
46
+ """A DataRecord, PyRecordModel, or AbstractRecordModel."""
47
+
40
48
 
41
49
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
42
50
  # FR-47575 - Reordered functions so that the Java and Python versions are as close to each other as possible.
@@ -524,9 +532,11 @@ class RecordHandler:
524
532
  """
525
533
  warnings.warn("Deprecated in favor of the [System/Custom/Quick]ReportRecordAutoPager classes.", DeprecationWarning)
526
534
  if isinstance(report_name, str):
535
+ # noinspection PyDeprecation
527
536
  results: list[dict[str, FieldValue]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
528
537
  page_limit, page_size, page_number)
529
538
  elif isinstance(report_name, RawReportTerm):
539
+ # noinspection PyDeprecation
530
540
  results: list[dict[str, FieldValue]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
531
541
  page_limit, page_size, page_number)
532
542
  elif isinstance(report_name, CustomReportCriteria):
@@ -539,6 +549,7 @@ class RecordHandler:
539
549
  # Enforce that the given custom report has a record ID column.
540
550
  if not any([x.data_type_name == dt and x.data_field_name == "RecordId" for x in report_name.column_list]):
541
551
  report_name.column_list.append(ReportColumn(dt, "RecordId", FieldType.LONG))
552
+ # noinspection PyDeprecation
542
553
  results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, report_name, filters,
543
554
  page_limit, page_size, page_number)
544
555
  else:
@@ -551,7 +562,7 @@ class RecordHandler:
551
562
  return self.query_models_by_id(wrapper_type, ids)
552
563
 
553
564
  @staticmethod
554
- def map_by_id(models: Iterable[SapioRecord]) -> dict[int, SapioRecord]:
565
+ def map_by_id(models: Iterable[IsSapioRecord]) -> dict[int, IsSapioRecord]:
555
566
  """
556
567
  Map the given records their record IDs.
557
568
 
@@ -560,12 +571,12 @@ class RecordHandler:
560
571
  """
561
572
  ret_dict: dict[int, SapioRecord] = {}
562
573
  for model in models:
563
- ret_dict.update({model.record_id: model})
574
+ ret_dict.update({AliasUtil.to_record_id(model): model})
564
575
  return ret_dict
565
576
 
566
577
  @staticmethod
567
- def map_by_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
568
- -> dict[FieldValue, list[SapioRecord]]:
578
+ def map_by_field(models: Iterable[IsSapioRecord], field_name: FieldIdentifier) \
579
+ -> dict[FieldValue, list[IsSapioRecord]]:
569
580
  """
570
581
  Map the given records by one of their fields. If any two records share the same field value, they'll appear in
571
582
  the same value list.
@@ -582,8 +593,8 @@ class RecordHandler:
582
593
  return ret_dict
583
594
 
584
595
  @staticmethod
585
- def map_by_unique_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
586
- -> dict[FieldValue, SapioRecord]:
596
+ def map_by_unique_field(models: Iterable[IsSapioRecord], field_name: FieldIdentifier) \
597
+ -> dict[FieldValue, IsSapioRecord]:
587
598
  """
588
599
  Uniquely map the given records by one of their fields. If any two records share the same field value, throws
589
600
  an exception.
@@ -662,7 +673,7 @@ class RecordHandler:
662
673
  return RecordHandler.sum_of_field(models, field_name) / len(models)
663
674
 
664
675
  @staticmethod
665
- def get_newest_record(records: Iterable[SapioRecord]) -> SapioRecord:
676
+ def get_newest_record(records: Iterable[IsSapioRecord]) -> IsSapioRecord:
666
677
  """
667
678
  Get the newest record from a list of records.
668
679
 
@@ -673,7 +684,7 @@ class RecordHandler:
673
684
 
674
685
  # FR-46696: Add a function for getting the oldest record in a list, just like we have one for the newest record.
675
686
  @staticmethod
676
- def get_oldest_record(records: Iterable[SapioRecord]) -> SapioRecord:
687
+ def get_oldest_record(records: Iterable[IsSapioRecord]) -> IsSapioRecord:
677
688
  """
678
689
  Get the oldest record from a list of records.
679
690
 
@@ -683,7 +694,7 @@ class RecordHandler:
683
694
  return min(records, key=lambda x: x.record_id)
684
695
 
685
696
  @staticmethod
686
- def get_min_record(records: list[RecordModel], field: FieldIdentifier) -> RecordModel:
697
+ def get_min_record(records: list[IsSapioRecord], field: FieldIdentifier) -> IsSapioRecord:
687
698
  """
688
699
  Get the record model with the minimum value of a given field from a list of record models.
689
700
 
@@ -695,7 +706,7 @@ class RecordHandler:
695
706
  return min(records, key=lambda x: x.get_field_value(field))
696
707
 
697
708
  @staticmethod
698
- def get_max_record(records: list[RecordModel], field: FieldIdentifier) -> RecordModel:
709
+ def get_max_record(records: list[IsSapioRecord], field: FieldIdentifier) -> IsSapioRecord:
699
710
  """
700
711
  Get the record model with the maximum value of a given field from a list of record models.
701
712
 
@@ -870,7 +881,7 @@ class RecordHandler:
870
881
  parent_dt: str = AliasUtil.to_data_type_name(parent_type)
871
882
  wrapper: type[WrappedType] | None = parent_type if isinstance(parent_type, type) else None
872
883
  record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
873
- parent: PyRecordModel | None = record.get_parent_of_type(parent_dt)
884
+ parent: PyRecordModel | None = record.get(Parent.of_type_name(parent_dt))
874
885
  if parent is not None:
875
886
  return self.wrap_model(parent, wrapper) if wrapper else parent
876
887
  return record.add(Parent.create(wrapper)) if wrapper else record.add(Parent.create_by_name(parent_dt))
@@ -888,7 +899,7 @@ class RecordHandler:
888
899
  child_dt: str = AliasUtil.to_data_type_name(child_type)
889
900
  wrapper: type[WrappedType] | None = child_type if isinstance(child_type, type) else None
890
901
  record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
891
- child: PyRecordModel | None = record.get_child_of_type(child_dt)
902
+ child: PyRecordModel | None = record.get(Child.of_type_name(child_dt))
892
903
  if child is not None:
893
904
  return self.wrap_model(child, wrapper) if wrapper else child
894
905
  return record.add(Child.create(wrapper)) if wrapper else record.add(Child.create_by_name(child_dt))
@@ -908,7 +919,7 @@ class RecordHandler:
908
919
  side_link_field: str = AliasUtil.to_data_field_name(side_link_field)
909
920
  wrapper: type[WrappedType] | None = side_link_type if isinstance(side_link_type, type) else None
910
921
  record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
911
- side_link: PyRecordModel | None = record.get_forward_side_link(side_link_field)
922
+ side_link: PyRecordModel | None = record.get(ForwardSideLink.of(side_link_field))
912
923
  if side_link is not None:
913
924
  return self.wrap_model(side_link, wrapper) if wrapper else side_link
914
925
  side_link: WrappedType | PyRecordModel = self.add_model(side_link_type)
@@ -955,52 +966,63 @@ class RecordHandler:
955
966
  if child not in children:
956
967
  record.remove(Child.ref(child))
957
968
 
969
+ # CR-47717: Update the map_[to/by]_[relationship] functions to allow PyRecordModels to be provided and returned
970
+ # instead of only using WrappedRecordModels and wrapper types.
958
971
  @staticmethod
959
- def map_to_parent(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType])\
960
- -> dict[WrappedRecordModel, WrappedType]:
972
+ def map_to_parent(models: Iterable[IsRecordModel], parent_type: type[WrappedType] | str) \
973
+ -> dict[IsRecordModel, WrappedType | PyRecordModel]:
961
974
  """
962
975
  Map a list of record models to a single parent of a given type. The parents must already be loaded.
963
976
 
964
977
  :param models: A list of record models.
965
- :param parent_type: The record model wrapper of the parent.
978
+ :param parent_type: The record model wrapper or data type name of the parents. If a data type name is
979
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
966
980
  :return: A dict[ModelType, ParentType]. If an input model doesn't have a parent of the given parent type, then
967
981
  it will map to None.
968
982
  """
969
- return_dict: dict[WrappedRecordModel, WrappedType] = {}
983
+ return_dict: dict[RecordModel, WrappedType | PyRecordModel] = {}
970
984
  for model in models:
971
- return_dict[model] = model.get_parent_of_type(parent_type)
985
+ if isinstance(parent_type, str):
986
+ return_dict[model] = model.get(Parent.of_type_name(parent_type))
987
+ else:
988
+ return_dict[model] = model.get(Parent.of_type(parent_type))
972
989
  return return_dict
973
990
 
974
991
  @staticmethod
975
- def map_to_parents(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
976
- -> dict[WrappedRecordModel, list[WrappedType]]:
992
+ def map_to_parents(models: Iterable[IsRecordModel], parent_type: type[WrappedType] | str) \
993
+ -> dict[IsRecordModel, list[WrappedType] | list[PyRecordModel]]:
977
994
  """
978
995
  Map a list of record models to a list parents of a given type. The parents must already be loaded.
979
996
 
980
997
  :param models: A list of record models.
981
- :param parent_type: The record model wrapper of the parents.
998
+ :param parent_type: The record model wrapper or data type name of the parents. If a data type name is
999
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
982
1000
  :return: A dict[ModelType, list[ParentType]]. If an input model doesn't have a parent of the given parent type,
983
1001
  then it will map to an empty list.
984
1002
  """
985
- return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
1003
+ return_dict: dict[WrappedRecordModel, list[WrappedType] | list[PyRecordModel]] = {}
986
1004
  for model in models:
987
- return_dict[model] = model.get_parents_of_type(parent_type)
1005
+ if isinstance(parent_type, str):
1006
+ return_dict[model] = model.get(Parents.of_type_name(parent_type))
1007
+ else:
1008
+ return_dict[model] = model.get(Parents.of_type(parent_type))
988
1009
  return return_dict
989
1010
 
990
1011
  @staticmethod
991
- def map_by_parent(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
992
- -> dict[WrappedType, WrappedRecordModel]:
1012
+ def map_by_parent(models: Iterable[IsRecordModel], parent_type: type[WrappedType] | str) \
1013
+ -> dict[WrappedType | PyRecordModel, IsRecordModel]:
993
1014
  """
994
1015
  Take a list of record models and map them by their parent. Essentially an inversion of map_to_parent.
995
1016
  If two records share the same parent, an exception will be thrown. The parents must already be loaded.
996
1017
 
997
1018
  :param models: A list of record models.
998
- :param parent_type: The record model wrapper of the parents.
1019
+ :param parent_type: The record model wrapper or data type name of the parents. If a data type name is
1020
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
999
1021
  :return: A dict[ParentType, ModelType]. If an input model doesn't have a parent of the given parent type,
1000
1022
  then it will not be in the resulting dictionary.
1001
1023
  """
1002
- to_parent: dict[WrappedRecordModel, WrappedType] = RecordHandler.map_to_parent(models, parent_type)
1003
- by_parent: dict[WrappedType, WrappedRecordModel] = {}
1024
+ to_parent: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler.map_to_parent(models, parent_type)
1025
+ by_parent: dict[WrappedType | PyRecordModel, RecordModel] = {}
1004
1026
  for record, parent in to_parent.items():
1005
1027
  if parent is None:
1006
1028
  continue
@@ -1011,70 +1033,81 @@ class RecordHandler:
1011
1033
  return by_parent
1012
1034
 
1013
1035
  @staticmethod
1014
- def map_by_parents(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
1015
- -> dict[WrappedType, list[WrappedRecordModel]]:
1036
+ def map_by_parents(models: Iterable[IsRecordModel], parent_type: type[WrappedType] | str) \
1037
+ -> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
1016
1038
  """
1017
1039
  Take a list of record models and map them by their parents. Essentially an inversion of map_to_parents. Input
1018
1040
  models that share a parent will end up in the same list. The parents must already be loaded.
1019
1041
 
1020
1042
  :param models: A list of record models.
1021
- :param parent_type: The record model wrapper of the parents.
1043
+ :param parent_type: The record model wrapper or data type name of the parents. If a data type name is
1044
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1022
1045
  :return: A dict[ParentType, list[ModelType]]. If an input model doesn't have a parent of the given parent type,
1023
1046
  then it will not be in the resulting dictionary.
1024
1047
  """
1025
- to_parents: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler.map_to_parents(models, parent_type)
1026
- by_parents: dict[WrappedType, list[WrappedRecordModel]] = {}
1048
+ to_parents: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = RecordHandler\
1049
+ .map_to_parents(models, parent_type)
1050
+ by_parents: dict[WrappedType | PyRecordModel, list[RecordModel]] = {}
1027
1051
  for record, parents in to_parents.items():
1028
1052
  for parent in parents:
1029
1053
  by_parents.setdefault(parent, []).append(record)
1030
1054
  return by_parents
1031
1055
 
1032
1056
  @staticmethod
1033
- def map_to_child(models: Iterable[WrappedRecordModel], child_type: type[WrappedType])\
1034
- -> dict[WrappedRecordModel, WrappedType]:
1057
+ def map_to_child(models: Iterable[IsRecordModel], child_type: type[WrappedType] | str) \
1058
+ -> dict[IsRecordModel, WrappedType | PyRecordModel]:
1035
1059
  """
1036
1060
  Map a list of record models to a single child of a given type. The children must already be loaded.
1037
1061
 
1038
1062
  :param models: A list of record models.
1039
- :param child_type: The record model wrapper of the child.
1063
+ :param child_type: The record model wrapper or data type name of the children. If a data type name is
1064
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1040
1065
  :return: A dict[ModelType, ChildType]. If an input model doesn't have a child of the given child type, then
1041
1066
  it will map to None.
1042
1067
  """
1043
- return_dict: dict[WrappedRecordModel, WrappedType] = {}
1068
+ return_dict: dict[RecordModel, WrappedType | PyRecordModel] = {}
1044
1069
  for model in models:
1045
- return_dict[model] = model.get_child_of_type(child_type)
1070
+ if isinstance(child_type, str):
1071
+ return_dict[model] = model.get(Child.of_type_name(child_type))
1072
+ else:
1073
+ return_dict[model] = model.get(Child.of_type(child_type))
1046
1074
  return return_dict
1047
1075
 
1048
1076
  @staticmethod
1049
- def map_to_children(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
1050
- -> dict[WrappedRecordModel, list[WrappedType]]:
1077
+ def map_to_children(models: Iterable[IsRecordModel], child_type: type[WrappedType] | str) \
1078
+ -> dict[IsRecordModel, list[WrappedType] | PyRecordModel]:
1051
1079
  """
1052
1080
  Map a list of record models to a list children of a given type. The children must already be loaded.
1053
1081
 
1054
1082
  :param models: A list of record models.
1055
- :param child_type: The record model wrapper of the children.
1083
+ :param child_type: The record model wrapper or data type name of the children. If a data type name is
1084
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1056
1085
  :return: A dict[ModelType, list[ChildType]]. If an input model doesn't have children of the given child type,
1057
1086
  then it will map to an empty list.
1058
1087
  """
1059
- return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
1088
+ return_dict: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = {}
1060
1089
  for model in models:
1061
- return_dict[model] = model.get_children_of_type(child_type)
1090
+ if isinstance(child_type, str):
1091
+ return_dict[model] = model.get(Children.of_type_name(child_type))
1092
+ else:
1093
+ return_dict[model] = model.get(Children.of_type(child_type))
1062
1094
  return return_dict
1063
1095
 
1064
1096
  @staticmethod
1065
- def map_by_child(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
1066
- -> dict[WrappedType, WrappedRecordModel]:
1097
+ def map_by_child(models: Iterable[IsRecordModel], child_type: type[WrappedType] | str) \
1098
+ -> dict[WrappedType | str, IsRecordModel]:
1067
1099
  """
1068
1100
  Take a list of record models and map them by their children. Essentially an inversion of map_to_child.
1069
1101
  If two records share the same child, an exception will be thrown. The children must already be loaded.
1070
1102
 
1071
1103
  :param models: A list of record models.
1072
- :param child_type: The record model wrapper of the children.
1104
+ :param child_type: The record model wrapper or data type name of the children. If a data type name is
1105
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1073
1106
  :return: A dict[ChildType, ModelType]. If an input model doesn't have a child of the given child type,
1074
1107
  then it will not be in the resulting dictionary.
1075
1108
  """
1076
- to_child: dict[WrappedRecordModel, WrappedType] = RecordHandler.map_to_child(models, child_type)
1077
- by_child: dict[WrappedType, WrappedRecordModel] = {}
1109
+ to_child: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler.map_to_child(models, child_type)
1110
+ by_child: dict[WrappedType | PyRecordModel, RecordModel] = {}
1078
1111
  for record, child in to_child.items():
1079
1112
  if child is None:
1080
1113
  continue
@@ -1085,45 +1118,50 @@ class RecordHandler:
1085
1118
  return by_child
1086
1119
 
1087
1120
  @staticmethod
1088
- def map_by_children(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
1089
- -> dict[WrappedType, list[WrappedRecordModel]]:
1121
+ def map_by_children(models: Iterable[IsRecordModel], child_type: type[WrappedType] | str) \
1122
+ -> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
1090
1123
  """
1091
1124
  Take a list of record models and map them by their children. Essentially an inversion of map_to_children. Input
1092
1125
  models that share a child will end up in the same list. The children must already be loaded.
1093
1126
 
1094
1127
  :param models: A list of record models.
1095
- :param child_type: The record model wrapper of the children.
1128
+ :param child_type: The record model wrapper or data type name of the children. If a data type name is
1129
+ provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1096
1130
  :return: A dict[ChildType, list[ModelType]]. If an input model doesn't have children of the given child type,
1097
1131
  then it will not be in the resulting dictionary.
1098
1132
  """
1099
- to_children: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler.map_to_children(models, child_type)
1100
- by_children: dict[WrappedType, list[WrappedRecordModel]] = {}
1133
+ to_children: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = RecordHandler\
1134
+ .map_to_children(models, child_type)
1135
+ by_children: dict[WrappedType | PyRecordModel, list[RecordModel]] = {}
1101
1136
  for record, children in to_children.items():
1102
1137
  for child in children:
1103
1138
  by_children.setdefault(child, []).append(record)
1104
1139
  return by_children
1105
1140
 
1106
1141
  @staticmethod
1107
- def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1108
- side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
1142
+ def map_to_forward_side_link(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1143
+ side_link_type: type[WrappedType] | None) \
1144
+ -> dict[IsRecordModel, WrappedType | PyRecordModel]:
1109
1145
  """
1110
1146
  Map a list of record models to their forward side link. The forward side link must already be loaded.
1111
1147
 
1112
1148
  :param models: A list of record models.
1113
1149
  :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.
1150
+ :param side_link_type: The record model wrapper of the forward side link. If None, the side links will
1151
+ be returned as PyRecordModels instead of WrappedRecordModels.
1115
1152
  :return: A dict[ModelType, SlideLink]. If an input model doesn't have a forward side link of the given type,
1116
1153
  then it will map to None.
1117
1154
  """
1118
1155
  field_name: str = AliasUtil.to_data_field_name(field_name)
1119
- return_dict: dict[WrappedRecordModel, WrappedType] = {}
1156
+ return_dict: dict[RecordModel, WrappedType | PyRecordModel] = {}
1120
1157
  for model in models:
1121
- return_dict[model] = model.get_forward_side_link(field_name, side_link_type)
1158
+ return_dict[model] = model.get(ForwardSideLink.of(field_name, side_link_type))
1122
1159
  return return_dict
1123
1160
 
1124
1161
  @staticmethod
1125
- def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1126
- side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
1162
+ def map_by_forward_side_link(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1163
+ side_link_type: type[WrappedType] | None) \
1164
+ -> dict[WrappedType | PyRecordModel, IsRecordModel]:
1127
1165
  """
1128
1166
  Take a list of record models and map them by their forward side link. Essentially an inversion of
1129
1167
  map_to_forward_side_link, but if two records share the same forward link, an exception is thrown.
@@ -1131,14 +1169,15 @@ class RecordHandler:
1131
1169
 
1132
1170
  :param models: A list of record models.
1133
1171
  :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.
1172
+ :param side_link_type: The record model wrapper of the forward side links. If None, the side links will
1173
+ be returned as PyRecordModels instead of WrappedRecordModels.
1135
1174
  :return: A dict[SideLink, ModelType]. If an input model doesn't have a forward side link of the given type
1136
1175
  pointing to it, then it will not be in the resulting dictionary.
1137
1176
  """
1138
1177
  field_name: str = AliasUtil.to_data_field_name(field_name)
1139
- to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
1178
+ to_side_link: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler\
1140
1179
  .map_to_forward_side_link(models, field_name, side_link_type)
1141
- by_side_link: dict[WrappedType, WrappedRecordModel] = {}
1180
+ by_side_link: dict[WrappedType | PyRecordModel, RecordModel] = {}
1142
1181
  for record, side_link in to_side_link.items():
1143
1182
  if side_link is None:
1144
1183
  continue
@@ -1149,8 +1188,9 @@ class RecordHandler:
1149
1188
  return by_side_link
1150
1189
 
1151
1190
  @staticmethod
1152
- def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1153
- side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
1191
+ def map_by_forward_side_links(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1192
+ side_link_type: type[WrappedType] | None) \
1193
+ -> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
1154
1194
  """
1155
1195
  Take a list of record models and map them by their forward side link. Essentially an inversion of
1156
1196
  map_to_forward_side_link. Input models that share a forward side link will end up in the same list.
@@ -1158,14 +1198,15 @@ class RecordHandler:
1158
1198
 
1159
1199
  :param models: A list of record models.
1160
1200
  :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.
1201
+ :param side_link_type: The record model wrapper of the forward side links. If None, the side links will
1202
+ be returned as PyRecordModels instead of WrappedRecordModels.
1162
1203
  :return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
1163
1204
  pointing to it, then it will not be in the resulting dictionary.
1164
1205
  """
1165
1206
  field_name: str = AliasUtil.to_data_field_name(field_name)
1166
- to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
1207
+ to_side_link: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler\
1167
1208
  .map_to_forward_side_link(models, field_name, side_link_type)
1168
- by_side_link: dict[WrappedType, list[WrappedRecordModel]] = {}
1209
+ by_side_link: dict[WrappedType | PyRecordModel, list[RecordModel]] = {}
1169
1210
  for record, side_link in to_side_link.items():
1170
1211
  if side_link is None:
1171
1212
  continue
@@ -1173,8 +1214,9 @@ class RecordHandler:
1173
1214
  return by_side_link
1174
1215
 
1175
1216
  @staticmethod
1176
- def map_to_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1177
- side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
1217
+ def map_to_reverse_side_link(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1218
+ side_link_type: type[WrappedType] | str) \
1219
+ -> dict[IsRecordModel, WrappedType | PyRecordModel]:
1178
1220
  """
1179
1221
  Map a list of record models to the reverse side link of a given type. If a given record has more than one
1180
1222
  reverse side link of this type, an exception is thrown. The reverse side links must already be loaded.
@@ -1182,14 +1224,18 @@ class RecordHandler:
1182
1224
  :param models: A list of record models.
1183
1225
  :param field_name: The field name on the side linked model where the side link to the given record models is
1184
1226
  located.
1185
- :param side_link_type: The record model wrapper of the reverse side links.
1227
+ :param side_link_type: The record model wrapper or data type name of the reverse side links. If a data type
1228
+ name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1186
1229
  :return: A dict[ModelType, SideLink]. If an input model doesn't have reverse side links of the given type,
1187
1230
  then it will map to None.
1188
1231
  """
1189
1232
  field_name: str = AliasUtil.to_data_field_name(field_name)
1190
- return_dict: dict[WrappedRecordModel, WrappedType] = {}
1233
+ return_dict: dict[RecordModel, WrappedType | PyRecordModel] = {}
1191
1234
  for model in models:
1192
- links: list[WrappedType] = model.get_reverse_side_link(field_name, side_link_type)
1235
+ if isinstance(side_link_type, str):
1236
+ links: list[WrappedType] = model.get(ReverseSideLink.of(side_link_type, field_name))
1237
+ else:
1238
+ links: list[WrappedType] = model.get(ReverseSideLink.of_type(side_link_type, field_name))
1193
1239
  if len(links) > 1:
1194
1240
  raise SapioException(f"Model {model.data_type_name} {model.record_id} has more than one reverse link "
1195
1241
  f"of type {side_link_type.get_wrapper_data_type_name()}.")
@@ -1197,8 +1243,9 @@ class RecordHandler:
1197
1243
  return return_dict
1198
1244
 
1199
1245
  @staticmethod
1200
- def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1201
- side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, list[WrappedType]]:
1246
+ def map_to_reverse_side_links(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1247
+ side_link_type: type[WrappedType] | str) \
1248
+ -> dict[IsRecordModel, list[WrappedType] | list[PyRecordModel]]:
1202
1249
  """
1203
1250
  Map a list of record models to a list reverse side links of a given type. The reverse side links must already
1204
1251
  be loaded.
@@ -1206,19 +1253,24 @@ class RecordHandler:
1206
1253
  :param models: A list of record models.
1207
1254
  :param field_name: The field name on the side linked model where the side link to the given record models is
1208
1255
  located.
1209
- :param side_link_type: The record model wrapper of the reverse side links.
1256
+ :param side_link_type: The record model wrapper or data type name of the reverse side links. If a data type
1257
+ name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1210
1258
  :return: A dict[ModelType, list[SideLink]]. If an input model doesn't have reverse side links of the given type,
1211
1259
  then it will map to an empty list.
1212
1260
  """
1213
1261
  field_name: str = AliasUtil.to_data_field_name(field_name)
1214
- return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
1262
+ return_dict: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = {}
1215
1263
  for model in models:
1216
- return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
1264
+ if isinstance(side_link_type, str):
1265
+ return_dict[model] = model.get(ReverseSideLink.of(side_link_type, field_name))
1266
+ else:
1267
+ return_dict[model] = model.get(ReverseSideLink.of_type(side_link_type, field_name))
1217
1268
  return return_dict
1218
1269
 
1219
1270
  @staticmethod
1220
- def map_by_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1221
- side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
1271
+ def map_by_reverse_side_link(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1272
+ side_link_type: type[WrappedType] | str) \
1273
+ -> dict[WrappedType | PyRecordModel, IsRecordModel]:
1222
1274
  """
1223
1275
  Take a list of record models and map them by their reverse side link. Essentially an inversion of
1224
1276
  map_to_reverse_side_link. If two records share the same reverse side link, an exception is thrown.
@@ -1227,14 +1279,15 @@ class RecordHandler:
1227
1279
  :param models: A list of record models.
1228
1280
  :param field_name: The field name on the side linked model where the side link to the given record models is
1229
1281
  located.
1230
- :param side_link_type: The record model wrapper of the reverse side links.
1282
+ :param side_link_type: The record model wrapper or data type name of the reverse side links. If a data type
1283
+ name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1231
1284
  :return: A dict[SideLink, ModelType]. If an input model doesn't have a reverse side link of the given type
1232
1285
  pointing to it, then it will not be in the resulting dictionary.
1233
1286
  """
1234
1287
  field_name: str = AliasUtil.to_data_field_name(field_name)
1235
- to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
1288
+ to_side_link: dict[RecordModel, WrappedType | PyRecordModel] = RecordHandler\
1236
1289
  .map_to_reverse_side_link(models, field_name, side_link_type)
1237
- by_side_link: dict[WrappedType, WrappedRecordModel] = {}
1290
+ by_side_link: dict[WrappedType | PyRecordModel, RecordModel] = {}
1238
1291
  for record, side_link in to_side_link.items():
1239
1292
  if side_link is None:
1240
1293
  continue
@@ -1245,8 +1298,8 @@ class RecordHandler:
1245
1298
  return by_side_link
1246
1299
 
1247
1300
  @staticmethod
1248
- def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
1249
- side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
1301
+ def map_by_reverse_side_links(models: Iterable[IsRecordModel], field_name: FieldIdentifier,
1302
+ side_link_type: type[WrappedType] | str) -> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
1250
1303
  """
1251
1304
  Take a list of record models and map them by their reverse side links. Essentially an inversion of
1252
1305
  map_to_reverse_side_links. Input models that share a reverse side link will end up in the same list.
@@ -1255,7 +1308,8 @@ class RecordHandler:
1255
1308
  :param models: A list of record models.
1256
1309
  :param field_name: The field name on the side linked model where the side link to the given record models is
1257
1310
  located.
1258
- :param side_link_type: The record model wrapper of the reverse side links.
1311
+ :param side_link_type: The record model wrapper or data type name of the reverse side links. If a data type
1312
+ name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
1259
1313
  :return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
1260
1314
  pointing to it, then it will not be in the resulting dictionary.
1261
1315
  """
@@ -1270,9 +1324,9 @@ class RecordHandler:
1270
1324
 
1271
1325
  # FR-46155: Update relationship path traversing functions to be non-static and take in a wrapper type so that the
1272
1326
  # output can be wrapped instead of requiring the user to wrap the output.
1273
- def get_linear_path(self, models: Iterable[RecordModel], path: RelationshipPath,
1327
+ def get_linear_path(self, models: Iterable[IsRecordModel], path: RelationshipPath,
1274
1328
  wrapper_type: type[WrappedType] | None = None) \
1275
- -> dict[RecordModel, WrappedType | PyRecordModel | None]:
1329
+ -> dict[IsRecordModel, WrappedType | PyRecordModel | None]:
1276
1330
  """
1277
1331
  Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
1278
1332
  path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
@@ -1285,7 +1339,7 @@ class RecordHandler:
1285
1339
  :return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
1286
1340
  path couldn't be reached, the record will map to None.
1287
1341
  """
1288
- ret_dict: dict[RecordModel, WrappedType | None] = {}
1342
+ ret_dict: dict[RecordModel, WrappedType | PyRecordModel | None] = {}
1289
1343
  # PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
1290
1344
  path: list[RelationshipNode] = path.path
1291
1345
  for model in models:
@@ -1332,9 +1386,9 @@ class RecordHandler:
1332
1386
  ret_dict.update({model: self.wrap_model(current, wrapper_type) if current else None})
1333
1387
  return ret_dict
1334
1388
 
1335
- def get_branching_path(self, models: Iterable[RecordModel], path: RelationshipPath,
1389
+ def get_branching_path(self, models: Iterable[IsRecordModel], path: RelationshipPath,
1336
1390
  wrapper_type: type[WrappedType] | None = None)\
1337
- -> dict[RecordModel, list[WrappedType] | list[PyRecordModel]]:
1391
+ -> dict[IsRecordModel, list[WrappedType] | list[PyRecordModel]]:
1338
1392
  """
1339
1393
  Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
1340
1394
  path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
@@ -1347,7 +1401,7 @@ class RecordHandler:
1347
1401
  :return: Each record model mapped to the records at the end of the path starting from itself. If the end of the
1348
1402
  path couldn't be reached, the record will map to an empty list.
1349
1403
  """
1350
- ret_dict: dict[RecordModel, list[WrappedType]] = {}
1404
+ ret_dict: dict[RecordModel, list[WrappedType] | list[PyRecordModel]] = {}
1351
1405
  # PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
1352
1406
  path: list[RelationshipNode] = path.path
1353
1407
  for model in models:
@@ -1383,9 +1437,9 @@ class RecordHandler:
1383
1437
 
1384
1438
  # FR-46155: Create a relationship traversing function that returns a single function at the end of the path like
1385
1439
  # 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,
1440
+ def get_flat_path(self, models: Iterable[IsRecordModel], path: RelationshipPath,
1387
1441
  wrapper_type: type[WrappedType] | None = None) \
1388
- -> dict[RecordModel, WrappedType | PyRecordModel | None]:
1442
+ -> dict[IsRecordModel, WrappedType | PyRecordModel | None]:
1389
1443
  """
1390
1444
  Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
1391
1445
  path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
@@ -1402,7 +1456,7 @@ class RecordHandler:
1402
1456
  :return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
1403
1457
  path couldn't be reached, the record will map to None.
1404
1458
  """
1405
- ret_dict: dict[RecordModel, WrappedType | None] = {}
1459
+ ret_dict: dict[RecordModel, WrappedType | PyRecordModel | None] = {}
1406
1460
  # PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
1407
1461
  path: list[RelationshipNode] = path.path
1408
1462
  for model in models: