sapiopycommons 2025.3.27a461__py3-none-any.whl → 2025.4.3a467__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/callbacks/callback_util.py +67 -59
- sapiopycommons/callbacks/field_builder.py +2 -0
- sapiopycommons/customreport/auto_pagers.py +2 -1
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/experiment_cache.py +173 -0
- sapiopycommons/eln/experiment_handler.py +183 -113
- sapiopycommons/files/file_util.py +4 -4
- sapiopycommons/general/accession_service.py +2 -2
- sapiopycommons/general/data_structure_util.py +115 -0
- sapiopycommons/recordmodel/record_handler.py +356 -27
- sapiopycommons/rules/eln_rule_handler.py +8 -1
- sapiopycommons/rules/on_save_rule_handler.py +8 -1
- sapiopycommons/webhook/webhook_handlers.py +3 -0
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.3.27a461.dist-info → sapiopycommons-2025.4.3a467.dist-info}/METADATA +1 -1
- {sapiopycommons-2025.3.27a461.dist-info → sapiopycommons-2025.4.3a467.dist-info}/RECORD +18 -16
- {sapiopycommons-2025.3.27a461.dist-info → sapiopycommons-2025.4.3a467.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.3.27a461.dist-info → sapiopycommons-2025.4.3a467.dist-info}/licenses/LICENSE +0 -0
|
@@ -26,6 +26,7 @@ from sapiopylib.rest.pojo.eln.SapioELNEnums import ExperimentEntryStatus, ElnExp
|
|
|
26
26
|
ElnBaseDataType
|
|
27
27
|
from sapiopylib.rest.pojo.eln.eln_headings import ElnExperimentTab, ElnExperimentTabAddCriteria
|
|
28
28
|
from sapiopylib.rest.pojo.eln.field_set import ElnFieldSetInfo
|
|
29
|
+
from sapiopylib.rest.pojo.eln.protocol_template import ProtocolTemplateInfo
|
|
29
30
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
30
31
|
from sapiopylib.rest.pojo.webhook.WebhookDirective import ElnExperimentDirective
|
|
31
32
|
from sapiopylib.rest.pojo.webhook.WebhookResult import SapioWebhookResult
|
|
@@ -40,6 +41,7 @@ from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
|
40
41
|
from sapiopylib.rest.utils.recordmodel.properties import Child
|
|
41
42
|
|
|
42
43
|
from sapiopycommons.datatype.data_fields import SystemFields
|
|
44
|
+
from sapiopycommons.datatype.experiment_cache import ExperimentCacheManager
|
|
43
45
|
from sapiopycommons.eln.experiment_report_util import ExperimentReportUtil
|
|
44
46
|
from sapiopycommons.eln.experiment_tags import PLATE_DESIGNER_PLUGIN
|
|
45
47
|
from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIdentifier, UserIdentifier, \
|
|
@@ -51,8 +53,8 @@ from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
|
51
53
|
Step: TypeAlias = str | ElnEntryStep
|
|
52
54
|
"""An object representing an identifier to an ElnEntryStep. May be either the name of the step or the ElnEntryStep
|
|
53
55
|
itself."""
|
|
54
|
-
|
|
55
|
-
"""An object representing an identifier to an ElnExperimentTab. May be either the name of the tab or the
|
|
56
|
+
TabIdentifier: TypeAlias = int | str | ElnExperimentTab
|
|
57
|
+
"""An object representing an identifier to an ElnExperimentTab. May be either the ID or name of the tab, or the
|
|
56
58
|
ElnExperimentTab itself."""
|
|
57
59
|
ElnDataTypeFields: TypeAlias = AbstractVeloxFieldDefinition | ElnFieldSetInfo | str | int
|
|
58
60
|
"""An object representing an identifier to an ElnDataType field. These can be field definitions for ad hoc fields,
|
|
@@ -77,6 +79,8 @@ class ExperimentHandler:
|
|
|
77
79
|
# Managers.
|
|
78
80
|
_eln_man: ElnManager
|
|
79
81
|
"""The ELN manager. Used for updating the experiment and its steps."""
|
|
82
|
+
_exp_cache: ExperimentCacheManager
|
|
83
|
+
"""The experiment cache manager. Used for caching experiment-related information."""
|
|
80
84
|
_inst_man: RecordModelInstanceManager
|
|
81
85
|
"""The record model instance manager. Used for wrapping the data records of a step as record models."""
|
|
82
86
|
_rec_handler: RecordHandler
|
|
@@ -113,9 +117,6 @@ class ExperimentHandler:
|
|
|
113
117
|
_tabs_by_name: dict[str, ElnExperimentTab]
|
|
114
118
|
"""The tabs for this experiment by their name. Only cached when first accessed."""
|
|
115
119
|
|
|
116
|
-
_predefined_fields: dict[str, dict[str, AbstractVeloxFieldDefinition]]
|
|
117
|
-
"""A dictionary of predefined fields for each ELN data type. Only cached when first accessed."""
|
|
118
|
-
|
|
119
120
|
# Constants
|
|
120
121
|
_ENTRY_COMPLETE_STATUSES = [ExperimentEntryStatus.Completed, ExperimentEntryStatus.CompletedApproved]
|
|
121
122
|
"""The set of statuses that an ELN entry could have and be considered completed/submitted."""
|
|
@@ -173,6 +174,7 @@ class ExperimentHandler:
|
|
|
173
174
|
|
|
174
175
|
# Grab various managers that may be used.
|
|
175
176
|
self._eln_man = DataMgmtServer.get_eln_manager(self.user)
|
|
177
|
+
self._exp_cache = ExperimentCacheManager(self.user)
|
|
176
178
|
self._inst_man = RecordModelManager(self.user).instance_manager
|
|
177
179
|
self._rec_handler = RecordHandler(self.user)
|
|
178
180
|
|
|
@@ -284,30 +286,35 @@ class ExperimentHandler:
|
|
|
284
286
|
self._tabs.clear()
|
|
285
287
|
self._tabs_by_name.clear()
|
|
286
288
|
|
|
287
|
-
def add_entry_to_caches(self, entry: ExperimentEntry | ElnEntryStep) ->
|
|
289
|
+
def add_entry_to_caches(self, entry: ExperimentEntry | ElnEntryStep) -> ElnEntryStep:
|
|
288
290
|
"""
|
|
289
291
|
Add the given entry to the cache of steps for this experiment. This is necessary in order for certain methods to
|
|
290
292
|
work. You should only need to do this if you have created a new entry in your code using a method outside
|
|
291
293
|
of this ExperimentHandler.
|
|
292
294
|
|
|
293
295
|
:param entry: The entry to add to the cache.
|
|
296
|
+
:return: The entry that was added to the cache as an ElnEntryStep.
|
|
294
297
|
"""
|
|
295
298
|
if isinstance(entry, ExperimentEntry):
|
|
296
299
|
entry = ElnEntryStep(self._protocol, entry)
|
|
297
300
|
self._steps.update({entry.get_name(): entry})
|
|
298
301
|
self._steps_by_id.update({entry.get_id(): entry})
|
|
299
302
|
# Skipping the options cache. The get_step_options method will update the cache when necessary.
|
|
303
|
+
return entry
|
|
300
304
|
|
|
301
|
-
def add_entries_to_caches(self, entries: list[ExperimentEntry | ElnEntryStep]) ->
|
|
305
|
+
def add_entries_to_caches(self, entries: list[ExperimentEntry | ElnEntryStep]) -> list[ElnEntryStep]:
|
|
302
306
|
"""
|
|
303
307
|
Add the given entries to the cache of steps for this experiment. This is necessary in order for certain methods
|
|
304
308
|
to work. You should only need to do this if you have created a new entry in your code using a method outside
|
|
305
309
|
of this ExperimentHandler.
|
|
306
310
|
|
|
307
311
|
:param entries: The entries to add to the cache.
|
|
312
|
+
:return: The entries that were added to the cache as ElnEntrySteps.
|
|
308
313
|
"""
|
|
314
|
+
new_entries: list[ElnEntryStep] = []
|
|
309
315
|
for entry in entries:
|
|
310
|
-
self.add_entry_to_caches(entry)
|
|
316
|
+
new_entries.append(self.add_entry_to_caches(entry))
|
|
317
|
+
return new_entries
|
|
311
318
|
|
|
312
319
|
def add_tab_to_cache(self, tab: ElnExperimentTab) -> None:
|
|
313
320
|
"""
|
|
@@ -348,22 +355,10 @@ class ExperimentHandler:
|
|
|
348
355
|
:param active_templates_only: Whether only active templates should be queried for.
|
|
349
356
|
:return: The newly created experiment.
|
|
350
357
|
"""
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
for template in templates:
|
|
356
|
-
if template.template_name != template_name:
|
|
357
|
-
continue
|
|
358
|
-
if template_version is not None and template.template_version != template_version:
|
|
359
|
-
continue
|
|
360
|
-
launch_template = template
|
|
361
|
-
break
|
|
362
|
-
if launch_template is None:
|
|
363
|
-
raise SapioException(f"No template with the name \"{template_name}\"" +
|
|
364
|
-
("" if template_version is None else f" and the version {template_version}") +
|
|
365
|
-
f" found.")
|
|
366
|
-
|
|
358
|
+
launch_template: ElnTemplate = ExperimentCacheManager(context).get_experiment_template(template_name,
|
|
359
|
+
active_templates_only,
|
|
360
|
+
template_version,
|
|
361
|
+
first_match=True)
|
|
367
362
|
if experiment_name is None:
|
|
368
363
|
experiment_name: str = launch_template.display_name
|
|
369
364
|
if parent_record is not None:
|
|
@@ -787,8 +782,9 @@ class ExperimentHandler:
|
|
|
787
782
|
return
|
|
788
783
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
789
784
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
790
|
-
raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. "
|
|
791
|
-
f"
|
|
785
|
+
raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. ELN "
|
|
786
|
+
f"records that are committed to the system will automatically appear in the ELN entry "
|
|
787
|
+
f"with the matching data type name.")
|
|
792
788
|
if dt != step.get_data_type_names()[0]:
|
|
793
789
|
raise SapioException(f"Cannot add {dt} records to entry {step.get_name()} of type "
|
|
794
790
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -814,8 +810,9 @@ class ExperimentHandler:
|
|
|
814
810
|
return
|
|
815
811
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
816
812
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
817
|
-
|
|
818
|
-
|
|
813
|
+
# CR-47532: Add remove_step_records support for Experiment Detail and Sample Detail entries.
|
|
814
|
+
self.remove_eln_rows(step, records)
|
|
815
|
+
return
|
|
819
816
|
if dt != step.get_data_type_names()[0]:
|
|
820
817
|
raise SapioException(f"Cannot remove {dt} records from entry {step.get_name()} of type "
|
|
821
818
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -844,9 +841,14 @@ class ExperimentHandler:
|
|
|
844
841
|
step = self.__to_eln_step(step)
|
|
845
842
|
if records:
|
|
846
843
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
844
|
+
# CR-47532: Add set_step_records support for Experiment Detail and Sample Detail entries.
|
|
847
845
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
848
|
-
|
|
849
|
-
|
|
846
|
+
remove_rows: list[PyRecordModel] = []
|
|
847
|
+
for record in self.get_step_models(step):
|
|
848
|
+
if record not in records:
|
|
849
|
+
remove_rows.append(record)
|
|
850
|
+
self.remove_eln_rows(step, remove_rows)
|
|
851
|
+
return
|
|
850
852
|
if dt != step.get_data_type_names()[0]:
|
|
851
853
|
raise SapioException(f"Cannot set {dt} records for entry {step.get_name()} of type "
|
|
852
854
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -873,6 +875,23 @@ class ExperimentHandler:
|
|
|
873
875
|
step.eln_entry.record_id = AliasUtil.to_data_record(record).record_id
|
|
874
876
|
|
|
875
877
|
# FR-46496 - Provide functions for adding and removing rows from an ELN data type entry.
|
|
878
|
+
def add_eln_row(self, step: Step, wrapper_type: type[WrappedType] | None = None) -> WrappedType | PyRecordModel:
|
|
879
|
+
"""
|
|
880
|
+
Add a row to an ELNExperimentDetail or ELNSampleDetail table entry. The row will not appear in the system
|
|
881
|
+
until a record manager store and commit has occurred.
|
|
882
|
+
|
|
883
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
884
|
+
list of steps in the experiment and caches them.
|
|
885
|
+
|
|
886
|
+
:param step:
|
|
887
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
888
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
889
|
+
:param wrapper_type: Optionally wrap the ELN data type in a record model wrapper. If not provided, returns
|
|
890
|
+
an unwrapped PyRecordModel.
|
|
891
|
+
:return: The newly created row.
|
|
892
|
+
"""
|
|
893
|
+
return self.add_eln_rows(step, 1, wrapper_type)[0]
|
|
894
|
+
|
|
876
895
|
def add_eln_rows(self, step: Step, count: int, wrapper_type: type[WrappedType] | None = None) \
|
|
877
896
|
-> list[WrappedType] | list[PyRecordModel]:
|
|
878
897
|
"""
|
|
@@ -901,10 +920,64 @@ class ExperimentHandler:
|
|
|
901
920
|
return self._inst_man.wrap_list(records, wrapper_type)
|
|
902
921
|
return records
|
|
903
922
|
|
|
904
|
-
def
|
|
923
|
+
def add_sample_detail(self, step: Step, sample: RecordModel,
|
|
924
|
+
wrapper_type: type[WrappedType] | None = None) \
|
|
925
|
+
-> WrappedType | PyRecordModel:
|
|
905
926
|
"""
|
|
906
|
-
Add a
|
|
907
|
-
|
|
927
|
+
Add a sample detail to a sample detail entry while relating it to the input sample record.
|
|
928
|
+
|
|
929
|
+
:param step:
|
|
930
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
931
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
932
|
+
:param sample: The sample record to add the sample detail to.
|
|
933
|
+
:param wrapper_type: Optionally wrap the sample detail in a record model wrapper. If not provided, returns
|
|
934
|
+
an unwrapped PyRecordModel.
|
|
935
|
+
:return: The newly created sample detail.
|
|
936
|
+
"""
|
|
937
|
+
return self.add_sample_details(step, [sample], wrapper_type)[0]
|
|
938
|
+
|
|
939
|
+
def add_sample_details(self, step: Step, samples: list[RecordModel],
|
|
940
|
+
wrapper_type: type[WrappedType] | None = None) \
|
|
941
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
942
|
+
"""
|
|
943
|
+
Add sample details to a sample details entry while relating them to the input sample records.
|
|
944
|
+
|
|
945
|
+
:param step:
|
|
946
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
947
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
948
|
+
:param samples: The sample records to add the sample details to.
|
|
949
|
+
:param wrapper_type: Optionally wrap the sample details in a record model wrapper. If not provided, returns
|
|
950
|
+
an unwrapped PyRecordModel.
|
|
951
|
+
:return: The newly created sample details. The indices of the samples in the input list match the index of the
|
|
952
|
+
sample details in this list that they are related to.
|
|
953
|
+
"""
|
|
954
|
+
step = self.__to_eln_step(step)
|
|
955
|
+
if step.eln_entry.entry_type != ElnEntryType.Table:
|
|
956
|
+
raise SapioException("The provided step is not a table entry.")
|
|
957
|
+
dt: str = step.get_data_type_names()[0]
|
|
958
|
+
if not ElnBaseDataType.is_eln_type(dt) or ElnBaseDataType.get_base_type(dt) != ElnBaseDataType.SAMPLE_DETAIL:
|
|
959
|
+
raise SapioException("The provided step is not an ELNSampleDetail entry.")
|
|
960
|
+
records: list[PyRecordModel] = []
|
|
961
|
+
for sample in samples:
|
|
962
|
+
if sample.data_type_name != "Sample":
|
|
963
|
+
raise SapioException(f"Received a {sample.data_type_name} record when Sample records were expected.")
|
|
964
|
+
detail: PyRecordModel = sample.add(Child.create_by_name(dt))
|
|
965
|
+
detail.set_field_values({
|
|
966
|
+
"SampleId": sample.get_field_value("SampleId"),
|
|
967
|
+
"OtherSampleId": sample.get_field_value("OtherSampleId")
|
|
968
|
+
})
|
|
969
|
+
records.append(detail)
|
|
970
|
+
if wrapper_type:
|
|
971
|
+
return self._inst_man.wrap_list(records, wrapper_type)
|
|
972
|
+
return records
|
|
973
|
+
|
|
974
|
+
def remove_eln_row(self, step: Step, record: SapioRecord) -> None:
|
|
975
|
+
"""
|
|
976
|
+
Remove a row from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
|
|
977
|
+
records in the system that match the entry's data type. This means that removing rows from an ELN data type
|
|
978
|
+
table entry is equivalent to deleting the records for the rows.
|
|
979
|
+
|
|
980
|
+
The row will not be deleted in the system until a record manager store and commit has occurred.
|
|
908
981
|
|
|
909
982
|
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
910
983
|
list of steps in the experiment and caches them.
|
|
@@ -912,11 +985,11 @@ class ExperimentHandler:
|
|
|
912
985
|
:param step:
|
|
913
986
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
914
987
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
915
|
-
:param
|
|
916
|
-
|
|
917
|
-
|
|
988
|
+
:param record:
|
|
989
|
+
The record to remove from the given step.
|
|
990
|
+
The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
|
|
918
991
|
"""
|
|
919
|
-
|
|
992
|
+
self.remove_eln_rows(step, [record])
|
|
920
993
|
|
|
921
994
|
def remove_eln_rows(self, step: Step, records: list[SapioRecord]) -> None:
|
|
922
995
|
"""
|
|
@@ -959,61 +1032,6 @@ class ExperimentHandler:
|
|
|
959
1032
|
for record in record_models:
|
|
960
1033
|
record.delete()
|
|
961
1034
|
|
|
962
|
-
def remove_eln_row(self, step: Step, record: SapioRecord) -> None:
|
|
963
|
-
"""
|
|
964
|
-
Remove a row from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
|
|
965
|
-
records in the system that match the entry's data type. This means that removing rows from an ELN data type
|
|
966
|
-
table entry is equivalent to deleting the records for the rows.
|
|
967
|
-
|
|
968
|
-
The row will not be deleted in the system until a record manager store and commit has occurred.
|
|
969
|
-
|
|
970
|
-
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
971
|
-
list of steps in the experiment and caches them.
|
|
972
|
-
|
|
973
|
-
:param step:
|
|
974
|
-
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
975
|
-
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
976
|
-
:param record:
|
|
977
|
-
The record to remove from the given step.
|
|
978
|
-
The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
|
|
979
|
-
"""
|
|
980
|
-
self.remove_eln_rows(step, [record])
|
|
981
|
-
|
|
982
|
-
def add_sample_details(self, step: Step, samples: list[RecordModel],
|
|
983
|
-
wrapper_type: type[WrappedType] | None = None) \
|
|
984
|
-
-> list[WrappedType] | list[PyRecordModel]:
|
|
985
|
-
"""
|
|
986
|
-
Add sample details to a sample details entry while relating them to the input sample records.
|
|
987
|
-
|
|
988
|
-
:param step:
|
|
989
|
-
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
990
|
-
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
991
|
-
:param samples: The sample records to add the sample details to.
|
|
992
|
-
:param wrapper_type: Optionally wrap the sample details in a record model wrapper. If not provided, returns
|
|
993
|
-
an unwrapped PyRecordModel.
|
|
994
|
-
:return: The newly created sample details. The indices of the samples in the input list match the index of the
|
|
995
|
-
sample details in this list that they are related to.
|
|
996
|
-
"""
|
|
997
|
-
step = self.__to_eln_step(step)
|
|
998
|
-
if step.eln_entry.entry_type != ElnEntryType.Table:
|
|
999
|
-
raise SapioException("The provided step is not a table entry.")
|
|
1000
|
-
dt: str = step.get_data_type_names()[0]
|
|
1001
|
-
if not ElnBaseDataType.is_eln_type(dt) or ElnBaseDataType.get_base_type(dt) != ElnBaseDataType.SAMPLE_DETAIL:
|
|
1002
|
-
raise SapioException("The provided step is not an ELNSampleDetail entry.")
|
|
1003
|
-
records: list[PyRecordModel] = []
|
|
1004
|
-
for sample in samples:
|
|
1005
|
-
if sample.data_type_name != "Sample":
|
|
1006
|
-
raise SapioException(f"Received a {sample.data_type_name} record when Sample records were expected.")
|
|
1007
|
-
detail: PyRecordModel = sample.add(Child.create_by_name(dt))
|
|
1008
|
-
detail.set_field_values({
|
|
1009
|
-
"SampleId": sample.get_field_value("SampleId"),
|
|
1010
|
-
"OtherSampleId": sample.get_field_value("OtherSampleId")
|
|
1011
|
-
})
|
|
1012
|
-
records.append(detail)
|
|
1013
|
-
if wrapper_type:
|
|
1014
|
-
return self._inst_man.wrap_list(records, wrapper_type)
|
|
1015
|
-
return records
|
|
1016
|
-
|
|
1017
1035
|
# noinspection PyPep8Naming
|
|
1018
1036
|
def update_step(self, step: Step,
|
|
1019
1037
|
entry_name: str | None = None,
|
|
@@ -1708,7 +1726,7 @@ class ExperimentHandler:
|
|
|
1708
1726
|
self.get_all_tabs()
|
|
1709
1727
|
return self._tabs_by_name[tab_name]
|
|
1710
1728
|
|
|
1711
|
-
def get_steps_in_tab(self, tab:
|
|
1729
|
+
def get_steps_in_tab(self, tab: TabIdentifier, data_type: DataTypeIdentifier | None = None) -> list[ElnEntryStep]:
|
|
1712
1730
|
"""
|
|
1713
1731
|
Get all the steps in the input tab sorted in order of appearance.
|
|
1714
1732
|
|
|
@@ -1718,7 +1736,8 @@ class ExperimentHandler:
|
|
|
1718
1736
|
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1719
1737
|
and caches them.
|
|
1720
1738
|
|
|
1721
|
-
:param tab: The tab
|
|
1739
|
+
:param tab: The tab to get the steps of. This can be either an ElnExperimentTab object, or the name or ID of
|
|
1740
|
+
the tab.
|
|
1722
1741
|
:param data_type: The data type to filter the steps by. If None, all steps are returned.
|
|
1723
1742
|
:return: A list of all the steps in the input tab sorted in order of appearance.
|
|
1724
1743
|
"""
|
|
@@ -1730,7 +1749,7 @@ class ExperimentHandler:
|
|
|
1730
1749
|
steps.sort(key=lambda s: (s.eln_entry.order, s.eln_entry.column_order))
|
|
1731
1750
|
return steps
|
|
1732
1751
|
|
|
1733
|
-
def get_next_entry_order_in_tab(self, tab:
|
|
1752
|
+
def get_next_entry_order_in_tab(self, tab: TabIdentifier) -> int:
|
|
1734
1753
|
"""
|
|
1735
1754
|
Get the next available order for a new entry in the input tab.
|
|
1736
1755
|
|
|
@@ -1740,12 +1759,71 @@ class ExperimentHandler:
|
|
|
1740
1759
|
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1741
1760
|
and caches them.
|
|
1742
1761
|
|
|
1743
|
-
:param tab: The tab
|
|
1762
|
+
:param tab: The tab to get the next entry order of. This can be either an ElnExperimentTab object, or the name
|
|
1763
|
+
or ID of the tab.
|
|
1744
1764
|
:return: The next available order for a new entry in the input tab.
|
|
1745
1765
|
"""
|
|
1746
1766
|
steps = self.get_steps_in_tab(tab)
|
|
1747
1767
|
return steps[-1].eln_entry.order + 1 if steps else 0
|
|
1748
1768
|
|
|
1769
|
+
# FR-47530: Add functions for dealing with entry positioning.
|
|
1770
|
+
def step_to_position(self, step: Step) -> ElnEntryPosition:
|
|
1771
|
+
"""
|
|
1772
|
+
Get the position of the input step in the experiment.
|
|
1773
|
+
|
|
1774
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1775
|
+
list of steps in the experiment and caches them.
|
|
1776
|
+
|
|
1777
|
+
:param step:
|
|
1778
|
+
The step to get the position of.
|
|
1779
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1780
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1781
|
+
:return: The position of the input step in the experiment.
|
|
1782
|
+
"""
|
|
1783
|
+
step: ElnEntryStep = self.__to_eln_step(step)
|
|
1784
|
+
entry: ExperimentEntry = step.eln_entry
|
|
1785
|
+
return ElnEntryPosition(entry.notebook_experiment_tab_id,
|
|
1786
|
+
entry.order,
|
|
1787
|
+
entry.column_span,
|
|
1788
|
+
entry.column_order)
|
|
1789
|
+
|
|
1790
|
+
def step_at_position(self, position: ElnEntryPosition) -> Step | None:
|
|
1791
|
+
"""
|
|
1792
|
+
Get the step at the input position in the experiment.
|
|
1793
|
+
|
|
1794
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1795
|
+
list of steps in the experiment and caches them.
|
|
1796
|
+
|
|
1797
|
+
:param position: The position to get the step at.
|
|
1798
|
+
:return: The step at the input position in the experiment, or None if no step exists at that position.
|
|
1799
|
+
"""
|
|
1800
|
+
if position.tab_id is None or position.order is None:
|
|
1801
|
+
raise SapioException("The provided position must at least have a tab ID and order.")
|
|
1802
|
+
for step in self.get_steps_in_tab(position.tab_id):
|
|
1803
|
+
entry: ExperimentEntry = step.eln_entry
|
|
1804
|
+
if entry.order != position.order:
|
|
1805
|
+
continue
|
|
1806
|
+
if position.column_span is not None and entry.column_span != position.column_span:
|
|
1807
|
+
continue
|
|
1808
|
+
if position.column_order is not None and entry.column_order != position.column_order:
|
|
1809
|
+
continue
|
|
1810
|
+
return step
|
|
1811
|
+
return None
|
|
1812
|
+
|
|
1813
|
+
# FR-47530: Create a function for adding protocol templates to the experiment.
|
|
1814
|
+
def add_protocol(self, protocol: ProtocolTemplateInfo | int, position: ElnEntryPosition) -> list[ElnEntryStep]:
|
|
1815
|
+
"""
|
|
1816
|
+
Add a protocol to the experiment. Updates the handler cache with the newly created entries.
|
|
1817
|
+
|
|
1818
|
+
:param protocol: The protocol to add. This can be either a ProtocolTemplateInfo object or the ID of the
|
|
1819
|
+
protocol template.
|
|
1820
|
+
:param position: The position that the protocol's first entry will be placed at.
|
|
1821
|
+
:return: The newly created protocol entries.
|
|
1822
|
+
"""
|
|
1823
|
+
protocol = protocol if isinstance(protocol, int) else protocol.template_id
|
|
1824
|
+
new_entries: list[ExperimentEntry] = self._eln_man.add_protocol_template(self._exp_id, protocol, position)
|
|
1825
|
+
return self.add_entries_to_caches(new_entries)
|
|
1826
|
+
|
|
1749
1827
|
# FR-47468: Add functions for creating new entries in the experiment.
|
|
1750
1828
|
def create_attachment_step(self, entry_name: str, data_type: DataTypeIdentifier,
|
|
1751
1829
|
*,
|
|
@@ -2119,24 +2197,12 @@ class ExperimentHandler:
|
|
|
2119
2197
|
for field in fields:
|
|
2120
2198
|
if isinstance(field, AbstractVeloxFieldDefinition):
|
|
2121
2199
|
field_defs.append(field)
|
|
2122
|
-
elif isinstance(field, ElnFieldSetInfo):
|
|
2123
|
-
field_defs.extend(self.
|
|
2200
|
+
elif isinstance(field, (int, ElnFieldSetInfo)):
|
|
2201
|
+
field_defs.extend(self._exp_cache.get_field_set_fields(field))
|
|
2124
2202
|
elif isinstance(field, str):
|
|
2125
|
-
field_defs.append(self.
|
|
2126
|
-
elif isinstance(field, int):
|
|
2127
|
-
field_defs.extend(self._eln_man.get_predefined_fields_from_field_set_id(field))
|
|
2203
|
+
field_defs.append(self._exp_cache.get_predefined_field(field, dt))
|
|
2128
2204
|
return field_defs
|
|
2129
2205
|
|
|
2130
|
-
def _predefined_field(self, field_name: str, data_type: ElnBaseDataType) -> AbstractVeloxFieldDefinition:
|
|
2131
|
-
"""
|
|
2132
|
-
Get the predefined field of the given name for the given ELN data type.
|
|
2133
|
-
"""
|
|
2134
|
-
# TODO: Should this be in some sort of DataTypeCacheManager?
|
|
2135
|
-
if data_type not in self._predefined_fields:
|
|
2136
|
-
fields: list[AbstractVeloxFieldDefinition] = self._eln_man.get_predefined_fields(data_type)
|
|
2137
|
-
self._predefined_fields[data_type.data_type_name] = {x.data_field_name: x for x in fields}
|
|
2138
|
-
return self._predefined_fields[data_type.data_type_name][field_name]
|
|
2139
|
-
|
|
2140
2206
|
def __to_eln_step(self, step: Step) -> ElnEntryStep:
|
|
2141
2207
|
"""
|
|
2142
2208
|
Convert a variable that could be either a string or an ElnEntryStep to just an ElnEntryStep.
|
|
@@ -2147,7 +2213,7 @@ class ExperimentHandler:
|
|
|
2147
2213
|
"""
|
|
2148
2214
|
return self.get_step(step) if isinstance(step, str) else step
|
|
2149
2215
|
|
|
2150
|
-
def __to_eln_tab(self, tab:
|
|
2216
|
+
def __to_eln_tab(self, tab: TabIdentifier) -> ElnExperimentTab:
|
|
2151
2217
|
"""
|
|
2152
2218
|
Convert a variable that could be either a string or an ElnExperimentTab to just an ElnExperimentTab.
|
|
2153
2219
|
This will query and cache the tabs for the experiment if the input tab is a name and the tabs have not been
|
|
@@ -2155,4 +2221,8 @@ class ExperimentHandler:
|
|
|
2155
2221
|
|
|
2156
2222
|
:return: The input tab as an ElnExperimentTab.
|
|
2157
2223
|
"""
|
|
2158
|
-
|
|
2224
|
+
if isinstance(tab, str):
|
|
2225
|
+
return self.get_tab(tab)
|
|
2226
|
+
if isinstance(tab, int):
|
|
2227
|
+
return [x for x in self._tabs if x.tab_id == tab][0]
|
|
2228
|
+
return tab
|
|
@@ -327,10 +327,10 @@ class FileUtil:
|
|
|
327
327
|
:param files: A dictionary of file name to file data as a string or bytes.
|
|
328
328
|
:return: The bytes for a zip file containing the input files.
|
|
329
329
|
"""
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
330
|
+
with io.BytesIO() as zip_buffer:
|
|
331
|
+
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
|
332
|
+
for file_name, file_data in files.items():
|
|
333
|
+
zip_file.writestr(file_name, file_data)
|
|
334
334
|
return zip_buffer.getvalue()
|
|
335
335
|
|
|
336
336
|
# Deprecated functions:
|
|
@@ -199,7 +199,7 @@ class AccessionRequestId(AbstractAccessionServiceOperator):
|
|
|
199
199
|
|
|
200
200
|
Properties:
|
|
201
201
|
numberOfCharacters: Number of characters maximum in the request ID.
|
|
202
|
-
accessorName: This is a legacy variable from drum.getNextIdListByMapName(), which allows setting different "accessorName" from old system. We need this for
|
|
202
|
+
accessorName: This is a legacy variable from drum.getNextIdListByMapName(), which allows setting different "accessorName" from old system. We need this for compatibility patch for converting these to the new preference format.
|
|
203
203
|
"""
|
|
204
204
|
_num_of_characters: int
|
|
205
205
|
_accessor_name: str
|
|
@@ -341,7 +341,7 @@ class AccessionService:
|
|
|
341
341
|
def get_affixed_id_in_batch(self, data_type_name: str, data_field_name: str, num_ids: int, prefix: str | None,
|
|
342
342
|
suffix: str | None, num_digits: int | None, start_num: int = 1) -> list[str]:
|
|
343
343
|
"""
|
|
344
|
-
Get the batch affixed IDs that are maximal in cache and
|
|
344
|
+
Get the batch affixed IDs that are maximal in cache and contiguous for a particular datatype.datafield under a given format.
|
|
345
345
|
:param data_type_name: The datatype name to look for max ID
|
|
346
346
|
:param data_field_name: The datafield name to look for max ID
|
|
347
347
|
:param num_ids: The number of IDs to accession.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Iterable, Any, Collection
|
|
3
|
+
|
|
4
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ArrayTransformation(Enum):
|
|
8
|
+
"""
|
|
9
|
+
An enumeration of the different transformations that can be applied to a 2D array.
|
|
10
|
+
"""
|
|
11
|
+
ROTATE_CLOCKWISE = 0
|
|
12
|
+
ROTATE_COUNTER_CLOCKWISE = 1
|
|
13
|
+
ROTATE_180_DEGREES = 2
|
|
14
|
+
MIRROR_HORIZONTAL = 3
|
|
15
|
+
MIRROR_VERTICAL = 4
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# FR-47524: Create a DataStructureUtils class that implements various collection utility functions from our Java
|
|
19
|
+
# libraries.
|
|
20
|
+
class DataStructureUtil:
|
|
21
|
+
"""
|
|
22
|
+
Utility class for working with data structures. Copies from ListUtil, SetUtil, and various other classes in
|
|
23
|
+
our Java library.
|
|
24
|
+
"""
|
|
25
|
+
@staticmethod
|
|
26
|
+
def find_first_or_none(values: Iterable[Any]) -> Any | None:
|
|
27
|
+
"""
|
|
28
|
+
Get the first value from an iterable, or None if the iterable is empty.
|
|
29
|
+
|
|
30
|
+
:param values: An iterable of values.
|
|
31
|
+
:return: The first value from the input, or None if the input is empty.
|
|
32
|
+
"""
|
|
33
|
+
return next(iter(values), None)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def remove_null_values(values: Iterable[Any]) -> list[Any]:
|
|
37
|
+
"""
|
|
38
|
+
Remove null values from a list.
|
|
39
|
+
|
|
40
|
+
:param values: An iterable of values.
|
|
41
|
+
:return: A list containing all the non-null values from the input.
|
|
42
|
+
"""
|
|
43
|
+
return [value for value in values if value is not None]
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def transform_2d_array(values: Collection[Collection[Any]], transformation: ArrayTransformation) \
|
|
47
|
+
-> Collection[Collection[Any]]:
|
|
48
|
+
"""
|
|
49
|
+
Perform a transformation on a 2D list.
|
|
50
|
+
|
|
51
|
+
:param values: An iterable of iterables. The iterables should all be of the same size.
|
|
52
|
+
:param transformation: The transformation to apply to the input.
|
|
53
|
+
:return: A new 2D list containing the input transformed according to the specified transformation.
|
|
54
|
+
"""
|
|
55
|
+
x: int = len(values)
|
|
56
|
+
for row in values:
|
|
57
|
+
y = len(row)
|
|
58
|
+
if y != x:
|
|
59
|
+
raise SapioException(f"Input must be a square 2D array. The provided array has a length of {x} but "
|
|
60
|
+
f"at least one row has a length of {y}.")
|
|
61
|
+
|
|
62
|
+
match transformation:
|
|
63
|
+
case ArrayTransformation.ROTATE_CLOCKWISE:
|
|
64
|
+
return [list(row) for row in zip(*values[::-1])]
|
|
65
|
+
case ArrayTransformation.ROTATE_COUNTER_CLOCKWISE:
|
|
66
|
+
return [list(row) for row in zip(*values)][::-1]
|
|
67
|
+
case ArrayTransformation.ROTATE_180_DEGREES:
|
|
68
|
+
return [row[::-1] for row in values[::-1]]
|
|
69
|
+
case ArrayTransformation.MIRROR_HORIZONTAL:
|
|
70
|
+
return [list(row[::-1]) for row in values]
|
|
71
|
+
case ArrayTransformation.MIRROR_VERTICAL:
|
|
72
|
+
return values[::-1]
|
|
73
|
+
|
|
74
|
+
raise SapioException(f"Invalid transformation: {transformation}")
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def flatten_to_list(values: Iterable[Iterable[Any]]) -> list[Any]:
|
|
78
|
+
"""
|
|
79
|
+
Flatten a list of lists into a single list.
|
|
80
|
+
|
|
81
|
+
:param values: An iterable of iterables.
|
|
82
|
+
:return: A single list containing all the values from the input. Elements are in the order they appear in the
|
|
83
|
+
input.
|
|
84
|
+
"""
|
|
85
|
+
return [item for sublist in values for item in sublist]
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def flatten_to_set(values: Iterable[Iterable[Any]]) -> set[Any]:
|
|
89
|
+
"""
|
|
90
|
+
Flatten a list of lists into a single set.
|
|
91
|
+
|
|
92
|
+
:param values: An iterable of iterables.
|
|
93
|
+
:return: A single set containing all the values from the input. Elements are in the order they appear in the
|
|
94
|
+
input.
|
|
95
|
+
"""
|
|
96
|
+
return {item for subset in values for item in subset}
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def invert_dictionary(dictionary: dict[Any, Any], list_values: bool = False) \
|
|
100
|
+
-> dict[Any, Any] | dict[Any, list[Any]]:
|
|
101
|
+
"""
|
|
102
|
+
Invert a dictionary, swapping keys and values. Note that the values of the input dictionary must be hashable.
|
|
103
|
+
|
|
104
|
+
:param dictionary: A dictionary to invert.
|
|
105
|
+
:param list_values: If false, keys that share the same value in the input dictionary will be overwritten in
|
|
106
|
+
the output dictionary so that only the last key remains. If true, the values of the output dictionary will
|
|
107
|
+
be lists where input keys that share the same value will be stored together.
|
|
108
|
+
:return: A new dictionary with the keys and values swapped.
|
|
109
|
+
"""
|
|
110
|
+
if list_values:
|
|
111
|
+
inverted = {}
|
|
112
|
+
for key, value in dictionary.items():
|
|
113
|
+
inverted.setdefault(value, []).append(key)
|
|
114
|
+
return inverted
|
|
115
|
+
return {value: key for key, value in dictionary.items()}
|