sapiopycommons 2025.3.25a459__py3-none-any.whl → 2025.3.31a466__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 +191 -116
- sapiopycommons/files/file_util.py +4 -4
- sapiopycommons/general/accession_service.py +2 -2
- sapiopycommons/general/data_structure_util.py +115 -0
- sapiopycommons/general/sapio_links.py +4 -12
- 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.25a459.dist-info → sapiopycommons-2025.3.31a466.dist-info}/METADATA +1 -1
- {sapiopycommons-2025.3.25a459.dist-info → sapiopycommons-2025.3.31a466.dist-info}/RECORD +19 -20
- sapiopycommons/ai/__init__.py +0 -0
- sapiopycommons/ai/tool_of_tools.py +0 -917
- sapiopycommons/general/html_formatter.py +0 -456
- {sapiopycommons-2025.3.25a459.dist-info → sapiopycommons-2025.3.31a466.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.3.25a459.dist-info → sapiopycommons-2025.3.31a466.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:
|
|
@@ -590,6 +585,9 @@ class ExperimentHandler:
|
|
|
590
585
|
"""
|
|
591
586
|
Set the experiment's status to Completed. Makes a webservice call to update the experiment. Checks if the
|
|
592
587
|
experiment is already completed, and does nothing if so.
|
|
588
|
+
|
|
589
|
+
NOTE: This will cause the usual process tracking logic to run as if you'd clicked the "Complete Experiment"
|
|
590
|
+
toolbar button. This includes moving the in process samples forward to the next step in the process.
|
|
593
591
|
"""
|
|
594
592
|
if not self.experiment_is_complete():
|
|
595
593
|
self._protocol.complete_protocol()
|
|
@@ -600,9 +598,11 @@ class ExperimentHandler:
|
|
|
600
598
|
Set the experiment's status to Canceled. Makes a webservice call to update the experiment. Checks if the
|
|
601
599
|
experiment is already canceled, and does nothing if so.
|
|
602
600
|
|
|
603
|
-
NOTE: This will
|
|
604
|
-
|
|
605
|
-
|
|
601
|
+
NOTE: This will cause the usual process tracking logic to run as if you'd clicked the "Cancel Experiment"
|
|
602
|
+
toolbar button. This includes moving the in process samples back into the process queue for the current step.
|
|
603
|
+
|
|
604
|
+
On version 24.12 and earlier, this was not the case, as the process tracking logic was tied to the button
|
|
605
|
+
instead of being on the experiment status change.
|
|
606
606
|
"""
|
|
607
607
|
if not self.experiment_is_canceled():
|
|
608
608
|
self._protocol.cancel_protocol()
|
|
@@ -782,8 +782,9 @@ class ExperimentHandler:
|
|
|
782
782
|
return
|
|
783
783
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
784
784
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
785
|
-
raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. "
|
|
786
|
-
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.")
|
|
787
788
|
if dt != step.get_data_type_names()[0]:
|
|
788
789
|
raise SapioException(f"Cannot add {dt} records to entry {step.get_name()} of type "
|
|
789
790
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -809,8 +810,9 @@ class ExperimentHandler:
|
|
|
809
810
|
return
|
|
810
811
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
811
812
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
812
|
-
|
|
813
|
-
|
|
813
|
+
# CR-47532: Add remove_step_records support for Experiment Detail and Sample Detail entries.
|
|
814
|
+
self.remove_eln_rows(step, records)
|
|
815
|
+
return
|
|
814
816
|
if dt != step.get_data_type_names()[0]:
|
|
815
817
|
raise SapioException(f"Cannot remove {dt} records from entry {step.get_name()} of type "
|
|
816
818
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -839,9 +841,14 @@ class ExperimentHandler:
|
|
|
839
841
|
step = self.__to_eln_step(step)
|
|
840
842
|
if records:
|
|
841
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.
|
|
842
845
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
843
|
-
|
|
844
|
-
|
|
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
|
|
845
852
|
if dt != step.get_data_type_names()[0]:
|
|
846
853
|
raise SapioException(f"Cannot set {dt} records for entry {step.get_name()} of type "
|
|
847
854
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -868,6 +875,23 @@ class ExperimentHandler:
|
|
|
868
875
|
step.eln_entry.record_id = AliasUtil.to_data_record(record).record_id
|
|
869
876
|
|
|
870
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
|
+
|
|
871
895
|
def add_eln_rows(self, step: Step, count: int, wrapper_type: type[WrappedType] | None = None) \
|
|
872
896
|
-> list[WrappedType] | list[PyRecordModel]:
|
|
873
897
|
"""
|
|
@@ -896,10 +920,64 @@ class ExperimentHandler:
|
|
|
896
920
|
return self._inst_man.wrap_list(records, wrapper_type)
|
|
897
921
|
return records
|
|
898
922
|
|
|
899
|
-
def
|
|
923
|
+
def add_sample_detail(self, step: Step, sample: RecordModel,
|
|
924
|
+
wrapper_type: type[WrappedType] | None = None) \
|
|
925
|
+
-> WrappedType | PyRecordModel:
|
|
900
926
|
"""
|
|
901
|
-
Add a
|
|
902
|
-
|
|
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.
|
|
903
981
|
|
|
904
982
|
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
905
983
|
list of steps in the experiment and caches them.
|
|
@@ -907,11 +985,11 @@ class ExperimentHandler:
|
|
|
907
985
|
:param step:
|
|
908
986
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
909
987
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
910
|
-
:param
|
|
911
|
-
|
|
912
|
-
|
|
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.
|
|
913
991
|
"""
|
|
914
|
-
|
|
992
|
+
self.remove_eln_rows(step, [record])
|
|
915
993
|
|
|
916
994
|
def remove_eln_rows(self, step: Step, records: list[SapioRecord]) -> None:
|
|
917
995
|
"""
|
|
@@ -954,61 +1032,6 @@ class ExperimentHandler:
|
|
|
954
1032
|
for record in record_models:
|
|
955
1033
|
record.delete()
|
|
956
1034
|
|
|
957
|
-
def remove_eln_row(self, step: Step, record: SapioRecord) -> None:
|
|
958
|
-
"""
|
|
959
|
-
Remove a row from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
|
|
960
|
-
records in the system that match the entry's data type. This means that removing rows from an ELN data type
|
|
961
|
-
table entry is equivalent to deleting the records for the rows.
|
|
962
|
-
|
|
963
|
-
The row will not be deleted in the system until a record manager store and commit has occurred.
|
|
964
|
-
|
|
965
|
-
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
966
|
-
list of steps in the experiment and caches them.
|
|
967
|
-
|
|
968
|
-
:param step:
|
|
969
|
-
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
970
|
-
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
971
|
-
:param record:
|
|
972
|
-
The record to remove from the given step.
|
|
973
|
-
The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
|
|
974
|
-
"""
|
|
975
|
-
self.remove_eln_rows(step, [record])
|
|
976
|
-
|
|
977
|
-
def add_sample_details(self, step: Step, samples: list[RecordModel],
|
|
978
|
-
wrapper_type: type[WrappedType] | None = None) \
|
|
979
|
-
-> list[WrappedType] | list[PyRecordModel]:
|
|
980
|
-
"""
|
|
981
|
-
Add sample details to a sample details entry while relating them to the input sample records.
|
|
982
|
-
|
|
983
|
-
:param step:
|
|
984
|
-
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
985
|
-
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
986
|
-
:param samples: The sample records to add the sample details to.
|
|
987
|
-
:param wrapper_type: Optionally wrap the sample details in a record model wrapper. If not provided, returns
|
|
988
|
-
an unwrapped PyRecordModel.
|
|
989
|
-
:return: The newly created sample details. The indices of the samples in the input list match the index of the
|
|
990
|
-
sample details in this list that they are related to.
|
|
991
|
-
"""
|
|
992
|
-
step = self.__to_eln_step(step)
|
|
993
|
-
if step.eln_entry.entry_type != ElnEntryType.Table:
|
|
994
|
-
raise SapioException("The provided step is not a table entry.")
|
|
995
|
-
dt: str = step.get_data_type_names()[0]
|
|
996
|
-
if not ElnBaseDataType.is_eln_type(dt) or ElnBaseDataType.get_base_type(dt) != ElnBaseDataType.SAMPLE_DETAIL:
|
|
997
|
-
raise SapioException("The provided step is not an ELNSampleDetail entry.")
|
|
998
|
-
records: list[PyRecordModel] = []
|
|
999
|
-
for sample in samples:
|
|
1000
|
-
if sample.data_type_name != "Sample":
|
|
1001
|
-
raise SapioException(f"Received a {sample.data_type_name} record when Sample records were expected.")
|
|
1002
|
-
detail: PyRecordModel = sample.add(Child.create_by_name(dt))
|
|
1003
|
-
detail.set_field_values({
|
|
1004
|
-
"SampleId": sample.get_field_value("SampleId"),
|
|
1005
|
-
"OtherSampleId": sample.get_field_value("OtherSampleId")
|
|
1006
|
-
})
|
|
1007
|
-
records.append(detail)
|
|
1008
|
-
if wrapper_type:
|
|
1009
|
-
return self._inst_man.wrap_list(records, wrapper_type)
|
|
1010
|
-
return records
|
|
1011
|
-
|
|
1012
1035
|
# noinspection PyPep8Naming
|
|
1013
1036
|
def update_step(self, step: Step,
|
|
1014
1037
|
entry_name: str | None = None,
|
|
@@ -1703,7 +1726,7 @@ class ExperimentHandler:
|
|
|
1703
1726
|
self.get_all_tabs()
|
|
1704
1727
|
return self._tabs_by_name[tab_name]
|
|
1705
1728
|
|
|
1706
|
-
def get_steps_in_tab(self, tab:
|
|
1729
|
+
def get_steps_in_tab(self, tab: TabIdentifier, data_type: DataTypeIdentifier | None = None) -> list[ElnEntryStep]:
|
|
1707
1730
|
"""
|
|
1708
1731
|
Get all the steps in the input tab sorted in order of appearance.
|
|
1709
1732
|
|
|
@@ -1713,7 +1736,8 @@ class ExperimentHandler:
|
|
|
1713
1736
|
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1714
1737
|
and caches them.
|
|
1715
1738
|
|
|
1716
|
-
: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.
|
|
1717
1741
|
:param data_type: The data type to filter the steps by. If None, all steps are returned.
|
|
1718
1742
|
:return: A list of all the steps in the input tab sorted in order of appearance.
|
|
1719
1743
|
"""
|
|
@@ -1725,7 +1749,7 @@ class ExperimentHandler:
|
|
|
1725
1749
|
steps.sort(key=lambda s: (s.eln_entry.order, s.eln_entry.column_order))
|
|
1726
1750
|
return steps
|
|
1727
1751
|
|
|
1728
|
-
def get_next_entry_order_in_tab(self, tab:
|
|
1752
|
+
def get_next_entry_order_in_tab(self, tab: TabIdentifier) -> int:
|
|
1729
1753
|
"""
|
|
1730
1754
|
Get the next available order for a new entry in the input tab.
|
|
1731
1755
|
|
|
@@ -1735,12 +1759,71 @@ class ExperimentHandler:
|
|
|
1735
1759
|
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1736
1760
|
and caches them.
|
|
1737
1761
|
|
|
1738
|
-
: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.
|
|
1739
1764
|
:return: The next available order for a new entry in the input tab.
|
|
1740
1765
|
"""
|
|
1741
1766
|
steps = self.get_steps_in_tab(tab)
|
|
1742
1767
|
return steps[-1].eln_entry.order + 1 if steps else 0
|
|
1743
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
|
+
|
|
1744
1827
|
# FR-47468: Add functions for creating new entries in the experiment.
|
|
1745
1828
|
def create_attachment_step(self, entry_name: str, data_type: DataTypeIdentifier,
|
|
1746
1829
|
*,
|
|
@@ -2114,24 +2197,12 @@ class ExperimentHandler:
|
|
|
2114
2197
|
for field in fields:
|
|
2115
2198
|
if isinstance(field, AbstractVeloxFieldDefinition):
|
|
2116
2199
|
field_defs.append(field)
|
|
2117
|
-
elif isinstance(field, ElnFieldSetInfo):
|
|
2118
|
-
field_defs.extend(self.
|
|
2200
|
+
elif isinstance(field, (int, ElnFieldSetInfo)):
|
|
2201
|
+
field_defs.extend(self._exp_cache.get_field_set_fields(field))
|
|
2119
2202
|
elif isinstance(field, str):
|
|
2120
|
-
field_defs.append(self.
|
|
2121
|
-
elif isinstance(field, int):
|
|
2122
|
-
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))
|
|
2123
2204
|
return field_defs
|
|
2124
2205
|
|
|
2125
|
-
def _predefined_field(self, field_name: str, data_type: ElnBaseDataType) -> AbstractVeloxFieldDefinition:
|
|
2126
|
-
"""
|
|
2127
|
-
Get the predefined field of the given name for the given ELN data type.
|
|
2128
|
-
"""
|
|
2129
|
-
# TODO: Should this be in some sort of DataTypeCacheManager?
|
|
2130
|
-
if data_type not in self._predefined_fields:
|
|
2131
|
-
fields: list[AbstractVeloxFieldDefinition] = self._eln_man.get_predefined_fields(data_type)
|
|
2132
|
-
self._predefined_fields[data_type.data_type_name] = {x.data_field_name: x for x in fields}
|
|
2133
|
-
return self._predefined_fields[data_type.data_type_name][field_name]
|
|
2134
|
-
|
|
2135
2206
|
def __to_eln_step(self, step: Step) -> ElnEntryStep:
|
|
2136
2207
|
"""
|
|
2137
2208
|
Convert a variable that could be either a string or an ElnEntryStep to just an ElnEntryStep.
|
|
@@ -2142,7 +2213,7 @@ class ExperimentHandler:
|
|
|
2142
2213
|
"""
|
|
2143
2214
|
return self.get_step(step) if isinstance(step, str) else step
|
|
2144
2215
|
|
|
2145
|
-
def __to_eln_tab(self, tab:
|
|
2216
|
+
def __to_eln_tab(self, tab: TabIdentifier) -> ElnExperimentTab:
|
|
2146
2217
|
"""
|
|
2147
2218
|
Convert a variable that could be either a string or an ElnExperimentTab to just an ElnExperimentTab.
|
|
2148
2219
|
This will query and cache the tabs for the experiment if the input tab is a name and the tabs have not been
|
|
@@ -2150,4 +2221,8 @@ class ExperimentHandler:
|
|
|
2150
2221
|
|
|
2151
2222
|
:return: The input tab as an ElnExperimentTab.
|
|
2152
2223
|
"""
|
|
2153
|
-
|
|
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()}
|