sapiopycommons 2025.5.6a511__py3-none-any.whl → 2025.5.6a512__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.

Files changed (54) hide show
  1. sapiopycommons/ai/__init__.py +0 -0
  2. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.py +43 -0
  3. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.pyi +31 -0
  4. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2_grpc.py +24 -0
  5. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.py +123 -0
  6. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.pyi +598 -0
  7. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2_grpc.py +24 -0
  8. sapiopycommons/ai/api/plan/proto/step_output_pb2.py +45 -0
  9. sapiopycommons/ai/api/plan/proto/step_output_pb2.pyi +42 -0
  10. sapiopycommons/ai/api/plan/proto/step_output_pb2_grpc.py +24 -0
  11. sapiopycommons/ai/api/plan/proto/step_pb2.py +43 -0
  12. sapiopycommons/ai/api/plan/proto/step_pb2.pyi +43 -0
  13. sapiopycommons/ai/api/plan/proto/step_pb2_grpc.py +24 -0
  14. sapiopycommons/ai/api/plan/script/proto/script_pb2.py +53 -0
  15. sapiopycommons/ai/api/plan/script/proto/script_pb2.pyi +99 -0
  16. sapiopycommons/ai/api/plan/script/proto/script_pb2_grpc.py +153 -0
  17. sapiopycommons/ai/api/plan/tool/proto/entry_pb2.py +57 -0
  18. sapiopycommons/ai/api/plan/tool/proto/entry_pb2.pyi +96 -0
  19. sapiopycommons/ai/api/plan/tool/proto/entry_pb2_grpc.py +24 -0
  20. sapiopycommons/ai/api/plan/tool/proto/tool_pb2.py +67 -0
  21. sapiopycommons/ai/api/plan/tool/proto/tool_pb2.pyi +220 -0
  22. sapiopycommons/ai/api/plan/tool/proto/tool_pb2_grpc.py +154 -0
  23. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.py +39 -0
  24. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.pyi +32 -0
  25. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2_grpc.py +24 -0
  26. sapiopycommons/ai/protobuf_utils.py +454 -0
  27. sapiopycommons/ai/tool_service_base.py +708 -0
  28. sapiopycommons/callbacks/callback_util.py +64 -116
  29. sapiopycommons/callbacks/field_builder.py +0 -2
  30. sapiopycommons/customreport/auto_pagers.py +1 -2
  31. sapiopycommons/customreport/term_builder.py +1 -1
  32. sapiopycommons/datatype/pseudo_data_types.py +326 -349
  33. sapiopycommons/eln/experiment_handler.py +719 -336
  34. sapiopycommons/eln/plate_designer.py +2 -7
  35. sapiopycommons/files/file_util.py +4 -4
  36. sapiopycommons/general/accession_service.py +2 -2
  37. sapiopycommons/general/aliases.py +1 -4
  38. sapiopycommons/general/html_formatter.py +456 -0
  39. sapiopycommons/general/sapio_links.py +12 -4
  40. sapiopycommons/processtracking/custom_workflow_handler.py +1 -2
  41. sapiopycommons/recordmodel/record_handler.py +27 -357
  42. sapiopycommons/rules/eln_rule_handler.py +1 -8
  43. sapiopycommons/rules/on_save_rule_handler.py +1 -8
  44. sapiopycommons/webhook/webhook_handlers.py +0 -3
  45. sapiopycommons/webhook/webservice_handlers.py +2 -2
  46. {sapiopycommons-2025.5.6a511.dist-info → sapiopycommons-2025.5.6a512.dist-info}/METADATA +2 -2
  47. sapiopycommons-2025.5.6a512.dist-info/RECORD +91 -0
  48. sapiopycommons/eln/experiment_cache.py +0 -188
  49. sapiopycommons/eln/experiment_step_factory.py +0 -476
  50. sapiopycommons/eln/step_creation.py +0 -236
  51. sapiopycommons/general/data_structure_util.py +0 -115
  52. sapiopycommons-2025.5.6a511.dist-info/RECORD +0 -67
  53. {sapiopycommons-2025.5.6a511.dist-info → sapiopycommons-2025.5.6a512.dist-info}/WHEEL +0 -0
  54. {sapiopycommons-2025.5.6a511.dist-info → sapiopycommons-2025.5.6a512.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- import io
4
3
  import warnings
5
4
  from collections.abc import Iterable
6
- from typing import Collection
7
5
  from weakref import WeakValueDictionary
8
6
 
9
7
  from sapiopylib.rest.DataRecordManagerService import DataRecordManager
@@ -15,28 +13,19 @@ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
15
13
  from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
16
14
  from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
17
15
  QueryAllRecordsOfTypeAutoPager
18
- from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel, AbstractRecordModelPropertyGetter, \
19
- RecordModelPropertyType, AbstractRecordModelPropertyAdder, AbstractRecordModelPropertySetter, \
20
- AbstractRecordModelPropertyRemover
16
+ from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
21
17
  from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager, \
22
18
  RecordModelRelationshipManager
23
19
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, WrappedRecordModel
24
20
  from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
25
21
  RelationshipNodeType
26
22
  from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
27
- from sapiopylib.rest.utils.recordmodel.properties import Parents, Parent, Children, Child, ForwardSideLink
28
23
 
29
24
  from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
30
- FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey, DataTypeIdentifier
25
+ FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey
31
26
  from sapiopycommons.general.custom_report_util import CustomReportUtil
32
27
  from sapiopycommons.general.exceptions import SapioException
33
28
 
34
- # Aliases for longer name.
35
- _PropertyGetter = AbstractRecordModelPropertyGetter
36
- _PropertyAdder = AbstractRecordModelPropertyAdder
37
- _PropertyRemover = AbstractRecordModelPropertyRemover
38
- _PropertySetter = AbstractRecordModelPropertySetter
39
- _PropertyType = RecordModelPropertyType
40
29
 
41
30
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
42
31
  class RecordHandler:
@@ -69,11 +58,12 @@ class RecordHandler:
69
58
  """
70
59
  :param context: The current webhook context or a user object to send requests from.
71
60
  """
61
+ self.user = AliasUtil.to_sapio_user(context)
72
62
  if self.__initialized:
73
63
  return
74
64
  self.__initialized = True
75
65
 
76
- self.user = AliasUtil.to_sapio_user(context)
66
+ self.user = context if isinstance(context, SapioUser) else context.user
77
67
  self.dr_man = DataRecordManager(self.user)
78
68
  self.rec_man = RecordModelManager(self.user)
79
69
  self.inst_man = self.rec_man.instance_manager
@@ -114,10 +104,8 @@ class RecordHandler:
114
104
  return [self.wrap_model(x, wrapper_type) for x in records]
115
105
 
116
106
  # CR-47491: Support providing a data type name string to receive PyRecordModels instead of requiring a WrapperType.
117
- # CR-47523: Support a singular field value being provided for the value_list parameter.
118
107
  def query_models(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
119
- value_list: Iterable[FieldValue] | FieldValue,
120
- page_limit: int | None = None, page_size: int | None = None) \
108
+ value_list: Iterable[FieldValue], page_limit: int | None = None, page_size: int | None = None) \
121
109
  -> list[WrappedType] | list[PyRecordModel]:
122
110
  """
123
111
  Shorthand for using the data record manager to query for a list of data records by field value
@@ -125,9 +113,7 @@ class RecordHandler:
125
113
 
126
114
  :param wrapper_type: The record model wrapper to use, or the data type name of the records.
127
115
  :param field: The field to query on.
128
- :param value_list: The values of the field to query on, or a singular field value that will be automatically
129
- converted to a singleton list. Note that field values of None are not supported by this method and will be
130
- ignored. If you need to query for records with a null field value, use a custom report.
116
+ :param value_list: The values of the field to query on.
131
117
  :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
132
118
  only functions if you set a page size or the platform enforces a page size.
133
119
  :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
@@ -140,10 +126,8 @@ class RecordHandler:
140
126
  return self.query_models_with_criteria(wrapper_type, field, value_list, criteria, page_limit)[0]
141
127
 
142
128
  def query_and_map_models(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
143
- value_list: Iterable[FieldValue] | FieldValue,
144
- page_limit: int | None = None, page_size: int | None = None,
145
- *,
146
- mapping_field: FieldIdentifier | None = None) \
129
+ value_list: Iterable[FieldValue], page_limit: int | None = None,
130
+ page_size: int | None = None, *, mapping_field: FieldIdentifier | None = None) \
147
131
  -> dict[FieldValue, list[WrappedType] | list[PyRecordModel]]:
148
132
  """
149
133
  Shorthand for using query_models to search for records given values on a specific field and then using
@@ -151,9 +135,7 @@ class RecordHandler:
151
135
 
152
136
  :param wrapper_type: The record model wrapper to use, or the data type name of the records.
153
137
  :param field: The field to query and map on.
154
- :param value_list: The values of the field to query on, or a singular field value that will be automatically
155
- converted to a singleton list. Note that field values of None are not supported by this method and will be
156
- ignored. If you need to query for records with a null field value, use a custom report.
138
+ :param value_list: The values of the field to query on.
157
139
  :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
158
140
  only functions if you set a page size or the platform enforces a page size.
159
141
  :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
@@ -168,10 +150,8 @@ class RecordHandler:
168
150
  mapping_field)
169
151
 
170
152
  def query_and_unique_map_models(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
171
- value_list: Iterable[FieldValue] | FieldValue,
172
- page_limit: int | None = None, page_size: int | None = None,
173
- *,
174
- mapping_field: FieldIdentifier | None = None) \
153
+ value_list: Iterable[FieldValue], page_limit: int | None = None,
154
+ page_size: int | None = None, *, mapping_field: FieldIdentifier | None = None) \
175
155
  -> dict[FieldValue, WrappedType | PyRecordModel]:
176
156
  """
177
157
  Shorthand for using query_models to search for records given values on a specific field and then using
@@ -180,9 +160,7 @@ class RecordHandler:
180
160
 
181
161
  :param wrapper_type: The record model wrapper to use, or the data type name of the records.
182
162
  :param field: The field to query and map on.
183
- :param value_list: The values of the field to query on, or a singular field value that will be automatically
184
- converted to a singleton list. Note that field values of None are not supported by this method and will be
185
- ignored. If you need to query for records with a null field value, use a custom report.
163
+ :param value_list: The values of the field to query on.
186
164
  :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
187
165
  only functions if you set a page size or the platform enforces a page size.
188
166
  :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
@@ -197,7 +175,7 @@ class RecordHandler:
197
175
  mapping_field)
198
176
 
199
177
  def query_models_with_criteria(self, wrapper_type: type[WrappedType] | str, field: FieldIdentifier,
200
- value_list: Iterable[FieldValue] | FieldValue,
178
+ value_list: Iterable[FieldValue],
201
179
  paging_criteria: DataRecordPojoPageCriteria | None = None,
202
180
  page_limit: int | None = None) \
203
181
  -> tuple[list[WrappedType] | list[PyRecordModel], DataRecordPojoPageCriteria]:
@@ -207,9 +185,7 @@ class RecordHandler:
207
185
 
208
186
  :param wrapper_type: The record model wrapper to use, or the data type name of the records.
209
187
  :param field: The field to query on.
210
- :param value_list: The values of the field to query on, or a singular field value that will be automatically
211
- converted to a singleton list. Note that field values of None are not supported by this method and will be
212
- ignored. If you need to query for records with a null field value, use a custom report.
188
+ :param value_list: The values of the field to query on.
213
189
  :param paging_criteria: The paging criteria to start the query with.
214
190
  :param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
215
191
  possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
@@ -221,8 +197,6 @@ class RecordHandler:
221
197
  if isinstance(wrapper_type, str):
222
198
  wrapper_type = None
223
199
  field: str = AliasUtil.to_data_field_name(field)
224
- if isinstance(value_list, FieldValue):
225
- value_list: list[FieldValue] = [value_list]
226
200
  pager = QueryDataRecordsAutoPager(dt, field, list(value_list), self.user, paging_criteria)
227
201
  pager.max_page = page_limit
228
202
  return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
@@ -383,7 +357,7 @@ class RecordHandler:
383
357
  raise SapioException("Unrecognized report object.")
384
358
 
385
359
  # Using the bracket accessor because we want to throw an exception if RecordId doesn't exist in the report.
386
- # This should only possibly be the case with system reports, as quick reports will include the record ID, and
360
+ # This should only possibly be the case with system reports, as quick reports will include the record ID and
387
361
  # we forced any given custom report to have a record ID column.
388
362
  ids: list[int] = [row["RecordId"] for row in results]
389
363
  return self.query_models_by_id(wrapper_type, ids)
@@ -549,279 +523,6 @@ class RecordHandler:
549
523
  secondary_identifiers.update({primary_identifier: id_value})
550
524
  return self.create_models_with_data(wrapper_type, [secondary_identifiers])[0]
551
525
 
552
- # FR-47525: Add functions for getting and setting record image bytes.
553
- def get_record_image(self, record: SapioRecord) -> bytes:
554
- """
555
- Retrieve the record image for a given record.
556
-
557
- :param record: The record model to retrieve the image of.
558
- :return: The file bytes of the given record's image.
559
- """
560
- record: DataRecord = AliasUtil.to_data_record(record)
561
- with io.BytesIO() as data_sink:
562
- def consume_data(chunk: bytes):
563
- data_sink.write(chunk)
564
-
565
- self.dr_man.get_record_image(record, consume_data)
566
- data_sink.flush()
567
- data_sink.seek(0)
568
- file_bytes = data_sink.read()
569
- return file_bytes
570
-
571
- def set_record_image(self, record: SapioRecord, file_data: str | bytes) -> None:
572
- """
573
- Set the record image for a given record.
574
-
575
- :param record: The record model to set the image of.
576
- :param file_data: The file data of the image to set on the record.
577
- """
578
- record: DataRecord = AliasUtil.to_data_record(record)
579
- with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as stream:
580
- self.dr_man.set_record_image(record, stream)
581
-
582
- # FR-47522: Add RecordHandler functions that copy from the RecordModelUtil class in our Java utilities.
583
- @staticmethod
584
- def get_values_list(records: list[RecordModel], field: FieldIdentifier) -> list[FieldValue]:
585
- """
586
- Get a list of field values from a list of record models.
587
-
588
- :param records: The list of record models to get the field values from.
589
- :param field: The field to get the values of.
590
- :return: A list of field values from the input record models. The values are in the same order as the input
591
- record models.
592
- """
593
- field: str = AliasUtil.to_data_field_name(field)
594
- return [x.get_field_value(field) for x in records]
595
-
596
- @staticmethod
597
- def get_values_set(records: list[RecordModel], field: FieldIdentifier) -> set[FieldValue]:
598
- """
599
- Get a set of field values from a list of record models.
600
-
601
- :param records: The list of record models to get the field values from.
602
- :param field: The field to get the values of.
603
- :return: A set of field values from the input record models.
604
- """
605
- field: str = AliasUtil.to_data_field_name(field)
606
- return {x.get_field_value(field) for x in records}
607
-
608
- @staticmethod
609
- def set_values(records: list[RecordModel], field: FieldIdentifier, value: FieldValue) -> None:
610
- """
611
- Set the value of a field on a list of record models.
612
-
613
- :param records: The list of record models to set the field value on.
614
- :param field: The field to set the value of.
615
- :param value: The value to set the field to for all input records.
616
- """
617
- field: str = AliasUtil.to_data_field_name(field)
618
- for record in records:
619
- record.set_field_value(field, value)
620
-
621
- @staticmethod
622
- def get_min_record(records: list[RecordModel], field: FieldIdentifier) -> RecordModel:
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:
635
- """
636
- Get the record model with the maximum value of a given field from a list of record models.
637
-
638
- :param records: The list of record models to search through.
639
- :param field: The field to find the maximum value of.
640
- :return: The record model with the maximum value of the given field.
641
- """
642
- field: str = AliasUtil.to_data_field_name(field)
643
- return max(records, key=lambda x: x.get_field_value(field))
644
-
645
- @staticmethod
646
- def get_from_all(records: Iterable[RecordModel], getter: _PropertyGetter[_PropertyType]) -> list[_PropertyType]:
647
- """
648
- Use a getter property on all records in a list of record models. For example, you can iterate over a list of
649
- record models using a getter of Ancestors.of_type(SampleModel) to get all the SampleModel ancestors from each
650
- record.
651
-
652
- :param records: The list of record models to get the property from.
653
- :param getter: The getter to use to get the property from each record.
654
- :return: A list of the property values from the input record models. The value at the matching index of the
655
- input records is the results of using the getter on that record.
656
- """
657
- return [x.get(getter) for x in records]
658
-
659
- @staticmethod
660
- def set_on_all(records: Iterable[RecordModel], setter: _PropertySetter[_PropertyType]) -> list[_PropertyType]:
661
- """
662
- Use a setter property on all records in a list of record models. For example, you can iterate over a list of
663
- record models user a setter of ForwardSideLink.ref(field_name, record) to set a forward side link on each
664
- record.
665
-
666
- :param records: The list of record models to set the property on.
667
- :param setter: The setter to use to set the property on each record.
668
- :return: A list of the property values that were set on the input record models. The value at the matching index
669
- of the input records is the results of using the setter on that record.
670
- """
671
- return [x.set(setter) for x in records]
672
-
673
- @staticmethod
674
- def add_to_all(records: Iterable[RecordModel], adder: _PropertyAdder[_PropertyType]) -> list[_PropertyType]:
675
- """
676
- Use an adder property on all records in a list of record models. For example, you can iterate over a list of
677
- record models using an adder of Child.create(SampleModel) to create a new SampleModel child on each record.
678
-
679
- :param records: The list of record models to add the property to.
680
- :param adder: The adder to use to add the property to each record.
681
- :return: A list of the property values that were added to the input record models. The value at the matching
682
- index of the input records is the results of using the adder on that record.
683
- """
684
- return [x.add(adder) for x in records]
685
-
686
- @staticmethod
687
- def remove_from_all(records: Iterable[RecordModel], remover: _PropertyRemover[_PropertyType]) -> list[_PropertyType]:
688
- """
689
- Use a remover property on all records in a list of record models. For example, you can iterate over a list of
690
- record models using a remover of Parents.ref(records) to remove a list of parents from each record.
691
-
692
- :param records: The list of record models to remove the property from.
693
- :param remover: The remover to use to remove the property from each record.
694
- :return: A list of the property values that were removed from the input record models. The value at the matching
695
- index of the input records is the results of using the remover on that record.
696
- """
697
- return [x.remove(remover) for x in records]
698
-
699
- # FR-47527: Created functions for manipulating relationships between records,
700
- def get_extension(self, model: RecordModel, wrapper_type: type[WrappedType] | str) \
701
- -> WrappedType | PyRecordModel | None:
702
- """
703
- Given a record with an extension record related to it, return the extension record as a record model.
704
- This will retrieve an extension record without doing a webservice request to the server. The input record and
705
- extension record will be considered related to one another if you later use load_child or load_parent on the
706
- input record or extension record respectively.
707
-
708
- :param model: The record model to get the extension for.
709
- :param wrapper_type: The record model wrapper to use, or the data type name of the extension record. If a data
710
- type name is provided, the returned record will be a PyRecordModel instead of a WrappedRecordModel.
711
- :return: The extension record model for the input record model, or None if no extension record exists.
712
- """
713
- ext_dt: str = AliasUtil.to_data_type_name(wrapper_type)
714
- ext_fields: FieldMap = {}
715
- for field, value in AliasUtil.to_field_map(model).items():
716
- if field.startswith(ext_dt + "."):
717
- ext_fields[field.removeprefix(ext_dt + ".")] = value
718
- if not ext_fields or ext_fields.get("RecordId") is None:
719
- return None
720
- ext_rec: DataRecord = DataRecord(ext_dt, ext_fields.get("RecordId"), ext_fields)
721
- ext_model: WrappedType | PyRecordModel = self.wrap_model(ext_rec, wrapper_type)
722
- self._spoof_child_load(model, ext_model)
723
- self._spoof_parent_load(ext_model, model)
724
- return ext_model
725
-
726
- def get_or_add_parent(self, record: RecordModel, parent_type: type[WrappedType] | str) \
727
- -> WrappedType | PyRecordModel:
728
- """
729
- Given a record model, retrieve the singular parent record model of a given type. If a parent of the given type
730
- does not exist, a new one will be created. The parents of the given data type must already be loaded.
731
-
732
- :param record: The record model to get the parent of.
733
- :param parent_type: The record model wrapper of the parent, or the data type name of the parent. If a data type
734
- name is provided, the returned record will be a PyRecordModel instead of a WrappedRecordModel.
735
- :return: The parent record model of the given type.
736
- """
737
- parent_dt: str = AliasUtil.to_data_type_name(parent_type)
738
- wrapper: type[WrappedType] | None = parent_type if isinstance(parent_type, type) else None
739
- record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
740
- parent: PyRecordModel | None = record.get_parent_of_type(parent_dt)
741
- if parent is not None:
742
- return self.wrap_model(parent, wrapper) if wrapper else parent
743
- return record.add(Parent.create(wrapper)) if wrapper else record.add(Parent.create_by_name(parent_dt))
744
-
745
- def get_or_add_child(self, record: RecordModel, child_type: type[WrappedType] | str) -> WrappedType | PyRecordModel:
746
- """
747
- Given a record model, retrieve the singular child record model of a given type. If a child of the given type
748
- does not exist, a new one will be created. The children of the given data type must already be loaded.
749
-
750
- :param record: The record model to get the child of.
751
- :param child_type: The record model wrapper of the child, or the data type name of the child. If a data type
752
- name is provided, the returned record will be a PyRecordModel instead of a WrappedRecordModel.
753
- :return: The child record model of the given type.
754
- """
755
- child_dt: str = AliasUtil.to_data_type_name(child_type)
756
- wrapper: type[WrappedType] | None = child_type if isinstance(child_type, type) else None
757
- record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
758
- child: PyRecordModel | None = record.get_child_of_type(child_dt)
759
- if child is not None:
760
- return self.wrap_model(child, wrapper) if wrapper else child
761
- return record.add(Child.create(wrapper)) if wrapper else record.add(Child.create_by_name(child_dt))
762
-
763
- def get_or_add_side_link(self, record: RecordModel, side_link_field: FieldIdentifier,
764
- side_link_type: type[WrappedType] | str) -> WrappedType | PyRecordModel:
765
- """
766
- Given a record model, retrieve the singular side link record model of a given type. If a side link of the given
767
- type does not exist, a new one will be created. The side links of the given data type must already be loaded.
768
-
769
- :param record: The record model to get the side link of.
770
- :param side_link_field: The field name of the side link to get.
771
- :param side_link_type: The record model wrapper of the side link, or the data type name of the side link. If a
772
- data type name is provided, the returned record will be a PyRecordModel instead of a WrappedRecordModel.
773
- :return: The side link record model of the given type.
774
- """
775
- side_link_field: str = AliasUtil.to_data_field_name(side_link_field)
776
- wrapper: type[WrappedType] | None = side_link_type if isinstance(side_link_type, type) else None
777
- record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
778
- side_link: PyRecordModel | None = record.get_forward_side_link(side_link_field)
779
- if side_link is not None:
780
- return self.wrap_model(side_link, wrapper) if wrapper else side_link
781
- side_link: WrappedType | PyRecordModel = self.add_model(side_link_type)
782
- record.set(ForwardSideLink.ref(side_link_field, side_link))
783
- return side_link
784
-
785
- @staticmethod
786
- def set_parents(record: RecordModel, parents: Iterable[RecordModel], parent_type: DataTypeIdentifier) -> None:
787
- """
788
- Set the parents of a record model to a list of parent record models of a given type. The parents of the given
789
- data type must already be loaded. This method will add the parents to the record model if they are not already
790
- parents, and remove any existing parents that are not in the input list.
791
-
792
- :param record: The record model to set the parents of.
793
- :param parents: The list of parent record models to set as the parents of the input record model.
794
- :param parent_type: The data type identifier of the parent record models.
795
- """
796
- parent_dt: str = AliasUtil.to_data_type_name(parent_type)
797
- existing_parents: list[PyRecordModel] = record.get(Parents.of_type_name(parent_dt))
798
- for parent in parents:
799
- if parent not in existing_parents:
800
- record.add(Parent.ref(parent))
801
- for parent in existing_parents:
802
- if parent not in parents:
803
- record.remove(Parent.ref(parent))
804
-
805
- @staticmethod
806
- def set_children(record: RecordModel, children: Iterable[RecordModel], child_type: DataTypeIdentifier) -> None:
807
- """
808
- Set the children of a record model to a list of child record models of a given type. The children of the given
809
- data type must already be loaded. This method will add the children to the record model if they are not already
810
- children, and remove any existing children that are not in the input list.
811
-
812
- :param record: The record model to set the children of.
813
- :param children: The list of child record models to set as the children of the input record model.
814
- :param child_type: The data type identifier of the child record models.
815
- """
816
- child_dt: str = AliasUtil.to_data_type_name(child_type)
817
- existing_children: list[PyRecordModel] = record.get(Children.of_type_name(child_dt))
818
- for child in children:
819
- if child not in existing_children:
820
- record.add(Child.ref(child))
821
- for child in existing_children:
822
- if child not in children:
823
- record.remove(Child.ref(child))
824
-
825
526
  @staticmethod
826
527
  def map_to_parent(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType])\
827
528
  -> dict[WrappedRecordModel, WrappedType]:
@@ -1203,7 +904,7 @@ class RecordHandler:
1203
904
  return field_sum
1204
905
 
1205
906
  @staticmethod
1206
- def mean_of_field(models: Collection[SapioRecord], field_name: FieldIdentifier) -> float:
907
+ def mean_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
1207
908
  """
1208
909
  Calculate the mean of the numeric value of a given field across all input models. Excepts that all given models
1209
910
  have a value. If the field is an integer field, the value will be converted to a float.
@@ -1212,7 +913,7 @@ class RecordHandler:
1212
913
  :param field_name: The name of the numeric field to mean.
1213
914
  :return: The mean of the field values for the collection of models.
1214
915
  """
1215
- return RecordHandler.sum_of_field(models, field_name) / len(models)
916
+ return RecordHandler.sum_of_field(models, field_name) / len(list(models))
1216
917
 
1217
918
  @staticmethod
1218
919
  def get_newest_record(records: Iterable[SapioRecord]) -> SapioRecord:
@@ -1222,7 +923,11 @@ class RecordHandler:
1222
923
  :param records: The list of records.
1223
924
  :return: The input record with the highest record ID. None if the input list is empty.
1224
925
  """
1225
- return max(records, key=lambda x: x.record_id)
926
+ newest: SapioRecord | None = None
927
+ for record in records:
928
+ if newest is None or record.record_id > newest.record_id:
929
+ newest = record
930
+ return newest
1226
931
 
1227
932
  # FR-46696: Add a function for getting the oldest record in a list, just like we have one for the newest record.
1228
933
  @staticmethod
@@ -1233,7 +938,11 @@ class RecordHandler:
1233
938
  :param records: The list of records.
1234
939
  :return: The input record with the lowest record ID. None if the input list is empty.
1235
940
  """
1236
- return min(records, key=lambda x: x.record_id)
941
+ oldest: SapioRecord | None = None
942
+ for record in records:
943
+ if oldest is None or record.record_id < oldest.record_id:
944
+ oldest = record
945
+ return oldest
1237
946
 
1238
947
  @staticmethod
1239
948
  def values_to_field_maps(field_name: FieldIdentifier, values: Iterable[FieldValue],
@@ -1460,42 +1169,3 @@ class RecordHandler:
1460
1169
  if record_type != model_type:
1461
1170
  raise SapioException(f"Data record of type {record_type} cannot be wrapped by the record model wrapper "
1462
1171
  f"of type {model_type}")
1463
-
1464
- @staticmethod
1465
- def _spoof_child_load(model: RecordModel, child: RecordModel) -> None:
1466
- """
1467
- Spoof the loading of a child record on a record model. This is useful for when you have records that you know
1468
- are related but didn't use the relationship manager to load the relationship, which would make a webservice
1469
- call.
1470
- """
1471
- RecordHandler._spoof_children_load(model, [child])
1472
-
1473
- @staticmethod
1474
- def _spoof_children_load(model: RecordModel, children: list[RecordModel]) -> None:
1475
- """
1476
- Spoof the loading of child records on a record model. This is useful for when you have records that you know
1477
- are related but didn't use the relationship manager to load the relationship, which would make a webservice
1478
- """
1479
- model: PyRecordModel = RecordModelInstanceManager.unwrap(model)
1480
- child_dt: str = AliasUtil.to_singular_data_type_name(children)
1481
- # noinspection PyProtectedMember
1482
- model._mark_children_loaded(child_dt, RecordModelInstanceManager.unwrap_list(children))
1483
-
1484
- @staticmethod
1485
- def _spoof_parent_load(model: RecordModel, parent: RecordModel) -> None:
1486
- """
1487
- Spoof the loading of a parent record on a record model. This is useful for when you have records that you know
1488
- are related but didn't use the relationship manager to load the relationship, which would make a webservice
1489
- """
1490
- RecordHandler._spoof_parents_load(model, [parent])
1491
-
1492
- @staticmethod
1493
- def _spoof_parents_load(model: RecordModel, parents: list[RecordModel]) -> None:
1494
- """
1495
- Spoof the loading of parent records on a record model. This is useful for when you have records that you know
1496
- are related but didn't use the relationship manager to load the relationship, which would make a webservice
1497
- """
1498
- model: PyRecordModel = RecordModelInstanceManager.unwrap(model)
1499
- parent_dt: str = AliasUtil.to_singular_data_type_name(parents)
1500
- # noinspection PyProtectedMember
1501
- model._mark_children_loaded(parent_dt, RecordModelInstanceManager.unwrap_list(parents))
@@ -126,16 +126,12 @@ class ElnRuleHandler:
126
126
  """
127
127
  return list(self._entry_to_field_maps.keys())
128
128
 
129
- # CR-47529: Add info about HVDT behavior to the docstring of these functions.
130
129
  def get_records(self, data_type: DataTypeIdentifier, entry: str | None = None) -> list[DataRecord]:
131
130
  """
132
131
  Get records from the cached context with the given data type. Capable of being filtered to searching within
133
132
  the context of an entry name. If the given data type or entry does not exist in the context,
134
133
  returns an empty list.
135
134
 
136
- Note that if you are attempting to retrieve record that are high volume data types and are receiving nothing,
137
- the HVDTs may have been sent as field maps. Consider using the get_field_maps function if this occurs.
138
-
139
135
  :param data_type: The data type of the records to return.
140
136
  :param entry: The name of the entry to grab the records from. If None, returns the records that match the data
141
137
  type from every entry. If an entry is provided, but it does not exist in the context, returns an empty list.
@@ -154,7 +150,7 @@ class ElnRuleHandler:
154
150
 
155
151
  Field maps will only exist in the context if the data record that the fields are from is no longer accessible
156
152
  to the user. This can occur because the data record was deleted, or because the user does not have access to the
157
- record due to ACL. This can also occur under certain circumstances if the records are HVDTs.
153
+ record due to ACL.
158
154
 
159
155
  :param data_type: The data type of the field maps to return.
160
156
  :param entry: The name of the entry to grab the field maps from. If None, returns the field maps that match the
@@ -174,9 +170,6 @@ class ElnRuleHandler:
174
170
  within the context of an entry name. If the given data type or entry does not exist in the context,
175
171
  returns an empty list.
176
172
 
177
- Note that if you are attempting to retrieve record that are high volume data types and are receiving nothing,
178
- the HVDTs may have been sent as field maps. Consider using the get_field_maps function if this occurs.
179
-
180
173
  :param wrapper_type: The record model wrapper or data type name of the record to get from the context.
181
174
  :param entry: The name of the entry to grab the records from. If None, returns the records that match the data
182
175
  type from every entry. If an entry is provided, but it does not exist in the context, returns an empty list.
@@ -122,16 +122,12 @@ class OnSaveRuleHandler:
122
122
  """
123
123
  return list(self._base_id_to_field_maps.keys())
124
124
 
125
- # CR-47529: Add info about HVDT behavior to the docstring of these functions.
126
125
  def get_records(self, data_type: DataTypeIdentifier, record_id: int | None = None) -> list[DataRecord]:
127
126
  """
128
127
  Get records from the cached context with the given data type. Capable of being filtered to searching within
129
128
  the context of a record ID. If the given data type or record ID does not exist in the context,
130
129
  returns an empty list.
131
130
 
132
- Note that if you are attempting to retrieve record that are high volume data types and are receiving nothing,
133
- the HVDTs may have been sent as field maps. Consider using the get_field_maps function if this occurs.
134
-
135
131
  :param data_type: The data type of the records to return.
136
132
  :param record_id: The record ID of the base record to search from. If None, returns the records that match the
137
133
  data type from every ID. If an ID is provided, but it does not exist in the context, returns an empty list.
@@ -150,7 +146,7 @@ class OnSaveRuleHandler:
150
146
 
151
147
  Field maps will only exist in the context if the data record that the fields are from is no longer accessible
152
148
  to the user. This can occur because the data record was deleted, or because the user does not have access to the
153
- record due to ACL. This can also occur under certain circumstances if the records are HVDTs.
149
+ record due to ACL.
154
150
 
155
151
  :param data_type: The data type of the field maps to return.
156
152
  :param record_id: The record ID of the base record to search from. If None, returns the field maps that match
@@ -170,9 +166,6 @@ class OnSaveRuleHandler:
170
166
  the context of a record ID. If the given data type or record ID does not exist in the context,
171
167
  returns an empty list.
172
168
 
173
- Note that if you are attempting to retrieve record that are high volume data types and are receiving nothing,
174
- the HVDTs may have been sent as field maps. Consider using the get_field_maps function if this occurs.
175
-
176
169
  :param wrapper_type: The record model wrapper or data type name of the record to get from the context.
177
170
  :param record_id: The record ID of the base record to search from. If None, returns the records that match the
178
171
  data type from ID. If an ID is provided, but it does not exist in the context, returns an empty list.
@@ -220,9 +220,6 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
220
220
  else:
221
221
  self.custom_context = None
222
222
 
223
- # CR-47526: Set the dialog timeout to 1 hour by default. This can be overridden by the webhook.
224
- self.callback.set_dialog_timeout(3600)
225
-
226
223
  # Set the default display types, titles, and messages for each type of exception that can display a message.
227
224
  self.default_user_error_display_type = MessageDisplayType.TOASTER_WARNING
228
225
  self.default_critical_error_display_type = MessageDisplayType.DISPLAY_ERROR
@@ -3,7 +3,7 @@ import traceback
3
3
  from abc import abstractmethod, ABC
4
4
  from base64 import b64decode
5
5
  from logging import Logger
6
- from typing import Any, Mapping
6
+ from typing import Any
7
7
 
8
8
  from flask import request, Response, Request
9
9
  from sapiopylib.rest.DataRecordManagerService import DataRecordManager
@@ -122,7 +122,7 @@ class AbstractWebserviceHandler(AbstractWebhookHandler):
122
122
  """
123
123
  pass
124
124
 
125
- def authenticate_user(self, headers: Mapping[str, str]) -> SapioUser:
125
+ def authenticate_user(self, headers: dict[str, str]) -> SapioUser:
126
126
  """
127
127
  Authenticate a user for making requests to a Sapio server using the provided headers. If no user can be
128
128
  authenticated, then an exception will be thrown.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2025.5.6a511
3
+ Version: 2025.5.6a512
4
4
  Summary: Official Sapio Python API Utilities Package
5
5
  Project-URL: Homepage, https://github.com/sapiosciences
6
6
  Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
18
  Requires-Python: >=3.10
19
19
  Requires-Dist: databind>=4.5
20
- Requires-Dist: sapiopylib>=2025.4.17.264
20
+ Requires-Dist: sapiopylib>=2024.5.24.210
21
21
  Description-Content-Type: text/markdown
22
22
 
23
23