sapiopycommons 2025.5.9a518__py3-none-any.whl → 2025.5.12a519__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sapiopycommons might be problematic. Click here for more details.
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.py +43 -0
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.pyi +31 -0
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2_grpc.py +24 -0
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.py +123 -0
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.pyi +598 -0
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2_grpc.py +24 -0
- sapiopycommons/ai/api/plan/proto/step_output_pb2.py +45 -0
- sapiopycommons/ai/api/plan/proto/step_output_pb2.pyi +42 -0
- sapiopycommons/ai/api/plan/proto/step_output_pb2_grpc.py +24 -0
- sapiopycommons/ai/api/plan/proto/step_pb2.py +43 -0
- sapiopycommons/ai/api/plan/proto/step_pb2.pyi +43 -0
- sapiopycommons/ai/api/plan/proto/step_pb2_grpc.py +24 -0
- sapiopycommons/ai/api/plan/script/proto/script_pb2.py +53 -0
- sapiopycommons/ai/api/plan/script/proto/script_pb2.pyi +99 -0
- sapiopycommons/ai/api/plan/script/proto/script_pb2_grpc.py +153 -0
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2.py +57 -0
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2.pyi +96 -0
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2_grpc.py +24 -0
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2.py +67 -0
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2.pyi +220 -0
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2_grpc.py +154 -0
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.py +39 -0
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.pyi +32 -0
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2_grpc.py +24 -0
- sapiopycommons/ai/protobuf_utils.py +454 -0
- sapiopycommons/ai/tool_service_base.py +740 -0
- sapiopycommons/callbacks/callback_util.py +64 -116
- sapiopycommons/callbacks/field_builder.py +0 -2
- sapiopycommons/customreport/auto_pagers.py +1 -2
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/pseudo_data_types.py +326 -349
- sapiopycommons/eln/experiment_handler.py +719 -336
- sapiopycommons/eln/plate_designer.py +2 -7
- sapiopycommons/files/file_util.py +4 -4
- sapiopycommons/general/accession_service.py +2 -2
- sapiopycommons/general/aliases.py +1 -4
- sapiopycommons/general/html_formatter.py +456 -0
- sapiopycommons/general/sapio_links.py +12 -4
- sapiopycommons/processtracking/custom_workflow_handler.py +1 -2
- sapiopycommons/recordmodel/record_handler.py +27 -357
- sapiopycommons/rules/eln_rule_handler.py +1 -8
- sapiopycommons/rules/on_save_rule_handler.py +1 -8
- sapiopycommons/webhook/webhook_handlers.py +0 -3
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.5.9a518.dist-info → sapiopycommons-2025.5.12a519.dist-info}/METADATA +2 -2
- sapiopycommons-2025.5.12a519.dist-info/RECORD +91 -0
- sapiopycommons/ai/tool_of_tools.py +0 -809
- sapiopycommons/eln/experiment_cache.py +0 -188
- sapiopycommons/eln/experiment_step_factory.py +0 -476
- sapiopycommons/eln/step_creation.py +0 -236
- sapiopycommons/general/data_structure_util.py +0 -115
- sapiopycommons-2025.5.9a518.dist-info/RECORD +0 -69
- {sapiopycommons-2025.5.9a518.dist-info → sapiopycommons-2025.5.12a519.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.5.9a518.dist-info → sapiopycommons-2025.5.12a519.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
|
|
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
|
|
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 =
|
|
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] |
|
|
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
|
|
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] |
|
|
144
|
-
|
|
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
|
|
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] |
|
|
172
|
-
|
|
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
|
|
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]
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
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.
|
|
3
|
+
Version: 2025.5.12a519
|
|
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>=
|
|
20
|
+
Requires-Dist: sapiopylib>=2024.5.24.210
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
|
|
23
23
|
|