sapiopycommons 2025.6.16a562__py3-none-any.whl → 2025.6.18a563__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/__init__.py +0 -0
- 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 +790 -0
- sapiopycommons/callbacks/callback_util.py +332 -665
- sapiopycommons/callbacks/field_builder.py +0 -2
- sapiopycommons/chem/IndigoMolecules.py +1 -29
- sapiopycommons/chem/Molecules.py +3 -3
- 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/processtracking/custom_workflow_handler.py +1 -2
- sapiopycommons/recordmodel/record_handler.py +317 -657
- 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.6.16a562.dist-info → sapiopycommons-2025.6.18a563.dist-info}/METADATA +2 -2
- sapiopycommons-2025.6.18a563.dist-info/RECORD +91 -0
- 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.6.16a562.dist-info/RECORD +0 -68
- {sapiopycommons-2025.6.16a562.dist-info → sapiopycommons-2025.6.18a563.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.6.16a562.dist-info → sapiopycommons-2025.6.18a563.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,46 +5,58 @@ from collections.abc import Mapping, Iterable
|
|
|
5
5
|
from typing import TypeAlias
|
|
6
6
|
from weakref import WeakValueDictionary
|
|
7
7
|
|
|
8
|
-
from sapiopycommons.eln.experiment_cache import ExperimentCacheManager
|
|
9
|
-
from sapiopycommons.eln.experiment_report_util import ExperimentReportUtil
|
|
10
|
-
from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIdentifier, UserIdentifier, \
|
|
11
|
-
DataTypeIdentifier, RecordModel, ExperimentEntryIdentifier
|
|
12
|
-
from sapiopycommons.general.exceptions import SapioException
|
|
13
|
-
from sapiopycommons.general.time_util import TimeUtil
|
|
14
|
-
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
15
8
|
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
9
|
+
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
16
10
|
from sapiopylib.rest.ELNService import ElnManager
|
|
17
11
|
from sapiopylib.rest.User import SapioUser
|
|
18
12
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
13
|
+
from sapiopylib.rest.pojo.TableColumn import TableColumn
|
|
14
|
+
from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition
|
|
19
15
|
from sapiopylib.rest.pojo.eln.ElnEntryPosition import ElnEntryPosition
|
|
20
16
|
from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment, TemplateExperimentQueryPojo, ElnTemplate, \
|
|
21
17
|
InitializeNotebookExperimentPojo, ElnExperimentUpdateCriteria
|
|
22
18
|
from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry, ExperimentTableEntry, ExperimentFormEntry, \
|
|
23
19
|
ExperimentAttachmentEntry, ExperimentPluginEntry, ExperimentDashboardEntry, ExperimentTextEntry, \
|
|
24
|
-
ExperimentTempDataEntry
|
|
20
|
+
ExperimentTempDataEntry, EntryAttachment, EntryRecordAttachment
|
|
25
21
|
from sapiopylib.rest.pojo.eln.ExperimentEntryCriteria import AbstractElnEntryUpdateCriteria, \
|
|
26
22
|
ElnTableEntryUpdateCriteria, ElnFormEntryUpdateCriteria, ElnAttachmentEntryUpdateCriteria, \
|
|
27
23
|
ElnPluginEntryUpdateCriteria, ElnDashboardEntryUpdateCriteria, ElnTextEntryUpdateCriteria, \
|
|
28
|
-
ElnTempDataEntryUpdateCriteria
|
|
24
|
+
ElnTempDataEntryUpdateCriteria, ElnEntryCriteria
|
|
29
25
|
from sapiopylib.rest.pojo.eln.SapioELNEnums import ExperimentEntryStatus, ElnExperimentStatus, ElnEntryType, \
|
|
30
26
|
ElnBaseDataType
|
|
31
27
|
from sapiopylib.rest.pojo.eln.eln_headings import ElnExperimentTab, ElnExperimentTabAddCriteria
|
|
32
|
-
from sapiopylib.rest.pojo.eln.
|
|
28
|
+
from sapiopylib.rest.pojo.eln.field_set import ElnFieldSetInfo
|
|
33
29
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
34
30
|
from sapiopylib.rest.pojo.webhook.WebhookDirective import ElnExperimentDirective
|
|
35
31
|
from sapiopylib.rest.pojo.webhook.WebhookResult import SapioWebhookResult
|
|
36
32
|
from sapiopylib.rest.utils.Protocols import ElnEntryStep, ElnExperimentProtocol
|
|
33
|
+
from sapiopylib.rest.utils.plates.MultiLayerPlating import MultiLayerPlateConfig, MultiLayerPlateLayer, \
|
|
34
|
+
MultiLayerDataTypeConfig, MultiLayerReplicateConfig, MultiLayerDilutionConfig
|
|
35
|
+
from sapiopylib.rest.utils.plates.MultiLayerPlatingUtils import MultiLayerPlatingManager
|
|
36
|
+
from sapiopylib.rest.utils.plates.PlatingUtils import PlatingOrder
|
|
37
37
|
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
38
38
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelInstanceManager, RecordModelManager
|
|
39
39
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
40
40
|
from sapiopylib.rest.utils.recordmodel.properties import Child
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
from sapiopycommons.datatype.data_fields import SystemFields
|
|
43
|
+
from sapiopycommons.eln.experiment_report_util import ExperimentReportUtil
|
|
44
|
+
from sapiopycommons.eln.experiment_tags import PLATE_DESIGNER_PLUGIN
|
|
45
|
+
from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIdentifier, UserIdentifier, \
|
|
46
|
+
DataTypeIdentifier, RecordModel, FieldMap, FieldIdentifier
|
|
47
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
48
|
+
from sapiopycommons.general.time_util import TimeUtil
|
|
49
|
+
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
50
|
+
|
|
51
|
+
Step: TypeAlias = str | ElnEntryStep
|
|
52
|
+
"""An object representing an identifier to an ElnEntryStep. May be either the name of the step or the ElnEntryStep
|
|
53
|
+
itself."""
|
|
54
|
+
Tab: TypeAlias = str | ElnExperimentTab
|
|
55
|
+
"""An object representing an identifier to an ElnExperimentTab. May be either the name of the tab or the
|
|
56
|
+
ElnExperimentTab itself."""
|
|
57
|
+
ElnDataTypeFields: TypeAlias = AbstractVeloxFieldDefinition | ElnFieldSetInfo | str | int
|
|
58
|
+
"""An object representing an identifier to an ElnDataType field. These can be field definitions for ad hoc fields,
|
|
59
|
+
predefined field set info objects, predefined field names, or integers for predefined field set IDs."""
|
|
48
60
|
|
|
49
61
|
|
|
50
62
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
@@ -65,8 +77,6 @@ class ExperimentHandler:
|
|
|
65
77
|
# Managers.
|
|
66
78
|
_eln_man: ElnManager
|
|
67
79
|
"""The ELN manager. Used for updating the experiment and its steps."""
|
|
68
|
-
_exp_cache: ExperimentCacheManager
|
|
69
|
-
"""The experiment cache manager. Used for caching experiment-related information."""
|
|
70
80
|
_inst_man: RecordModelInstanceManager
|
|
71
81
|
"""The record model instance manager. Used for wrapping the data records of a step as record models."""
|
|
72
82
|
_rec_handler: RecordHandler
|
|
@@ -85,9 +95,7 @@ class ExperimentHandler:
|
|
|
85
95
|
|
|
86
96
|
_queried_all_steps: bool
|
|
87
97
|
"""Whether this ExperimentHandler has queried the system for all steps in the experiment."""
|
|
88
|
-
_steps:
|
|
89
|
-
"""The sorted list of steps for this experiment. All steps are cached the first time any individual step is accessed."""
|
|
90
|
-
_steps_by_name: dict[str, ElnEntryStep]
|
|
98
|
+
_steps: dict[str, ElnEntryStep]
|
|
91
99
|
"""Steps from this experiment by their name. All steps are cached the first time any individual step is accessed."""
|
|
92
100
|
_steps_by_id: dict[int, ElnEntryStep]
|
|
93
101
|
"""Steps from this experiment by their ID. All steps are cached the first time any individual step is accessed."""
|
|
@@ -101,12 +109,13 @@ class ExperimentHandler:
|
|
|
101
109
|
_queried_all_tabs: bool
|
|
102
110
|
"""Whether this ExperimentHandler has queried the system for all tabs in the experiment."""
|
|
103
111
|
_tabs: list[ElnExperimentTab]
|
|
104
|
-
"""The
|
|
105
|
-
_tabs_by_id: dict[int, ElnExperimentTab]
|
|
106
|
-
"""The tabs for this experiment by their ID. Only cached when first accessed."""
|
|
112
|
+
"""The tabs for this experiment. Only cached when first accessed."""
|
|
107
113
|
_tabs_by_name: dict[str, ElnExperimentTab]
|
|
108
114
|
"""The tabs for this experiment by their name. Only cached when first accessed."""
|
|
109
115
|
|
|
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
|
+
|
|
110
119
|
# Constants
|
|
111
120
|
_ENTRY_COMPLETE_STATUSES = [ExperimentEntryStatus.Completed, ExperimentEntryStatus.CompletedApproved]
|
|
112
121
|
"""The set of statuses that an ELN entry could have and be considered completed/submitted."""
|
|
@@ -164,20 +173,17 @@ class ExperimentHandler:
|
|
|
164
173
|
|
|
165
174
|
# Grab various managers that may be used.
|
|
166
175
|
self._eln_man = DataMgmtServer.get_eln_manager(self.user)
|
|
167
|
-
self._exp_cache = ExperimentCacheManager(self.user)
|
|
168
176
|
self._inst_man = RecordModelManager(self.user).instance_manager
|
|
169
177
|
self._rec_handler = RecordHandler(self.user)
|
|
170
178
|
|
|
171
179
|
# Create empty caches to fill when necessary.
|
|
172
180
|
self._queried_all_steps = False
|
|
173
|
-
self.
|
|
181
|
+
self._steps = {}
|
|
174
182
|
self._steps_by_id = {}
|
|
175
|
-
self._steps = []
|
|
176
183
|
self._step_options = {}
|
|
177
184
|
self._step_updates = {}
|
|
178
185
|
|
|
179
186
|
self._tabs = []
|
|
180
|
-
self._tabs_by_id = {}
|
|
181
187
|
self._tabs_by_name = {}
|
|
182
188
|
|
|
183
189
|
self._queried_all_tabs = False
|
|
@@ -193,8 +199,7 @@ class ExperimentHandler:
|
|
|
193
199
|
for entry in self.context.experiment_entry_list:
|
|
194
200
|
cache_steps.append(ElnEntryStep(self._protocol, entry))
|
|
195
201
|
for step in cache_steps:
|
|
196
|
-
self._steps.
|
|
197
|
-
self._steps_by_name.update({step.get_name(): step})
|
|
202
|
+
self._steps.update({step.get_name(): step})
|
|
198
203
|
self._steps_by_id.update({step.get_id(): step})
|
|
199
204
|
|
|
200
205
|
@staticmethod
|
|
@@ -259,7 +264,6 @@ class ExperimentHandler:
|
|
|
259
264
|
"""
|
|
260
265
|
self._queried_all_steps = False
|
|
261
266
|
self._steps.clear()
|
|
262
|
-
self._steps_by_name.clear()
|
|
263
267
|
self._steps_by_id.clear()
|
|
264
268
|
self._step_options.clear()
|
|
265
269
|
self._step_updates.clear()
|
|
@@ -278,39 +282,32 @@ class ExperimentHandler:
|
|
|
278
282
|
"""
|
|
279
283
|
self._queried_all_tabs = False
|
|
280
284
|
self._tabs.clear()
|
|
281
|
-
self._tabs_by_id.clear()
|
|
282
285
|
self._tabs_by_name.clear()
|
|
283
286
|
|
|
284
|
-
def add_entry_to_caches(self, entry: ExperimentEntry | ElnEntryStep) ->
|
|
287
|
+
def add_entry_to_caches(self, entry: ExperimentEntry | ElnEntryStep) -> None:
|
|
285
288
|
"""
|
|
286
289
|
Add the given entry to the cache of steps for this experiment. This is necessary in order for certain methods to
|
|
287
290
|
work. You should only need to do this if you have created a new entry in your code using a method outside
|
|
288
291
|
of this ExperimentHandler.
|
|
289
292
|
|
|
290
293
|
:param entry: The entry to add to the cache.
|
|
291
|
-
:return: The entry that was added to the cache as an ElnEntryStep.
|
|
292
294
|
"""
|
|
293
295
|
if isinstance(entry, ExperimentEntry):
|
|
294
296
|
entry = ElnEntryStep(self._protocol, entry)
|
|
295
|
-
self._steps.
|
|
296
|
-
self._steps_by_name.update({entry.get_name(): entry})
|
|
297
|
+
self._steps.update({entry.get_name(): entry})
|
|
297
298
|
self._steps_by_id.update({entry.get_id(): entry})
|
|
298
299
|
# Skipping the options cache. The get_step_options method will update the cache when necessary.
|
|
299
|
-
return entry
|
|
300
300
|
|
|
301
|
-
def add_entries_to_caches(self, entries: list[ExperimentEntry | ElnEntryStep]) ->
|
|
301
|
+
def add_entries_to_caches(self, entries: list[ExperimentEntry | ElnEntryStep]) -> None:
|
|
302
302
|
"""
|
|
303
303
|
Add the given entries to the cache of steps for this experiment. This is necessary in order for certain methods
|
|
304
304
|
to work. You should only need to do this if you have created a new entry in your code using a method outside
|
|
305
305
|
of this ExperimentHandler.
|
|
306
306
|
|
|
307
307
|
:param entries: The entries to add to the cache.
|
|
308
|
-
:return: The entries that were added to the cache as ElnEntrySteps.
|
|
309
308
|
"""
|
|
310
|
-
new_entries: list[ElnEntryStep] = []
|
|
311
309
|
for entry in entries:
|
|
312
|
-
|
|
313
|
-
return new_entries
|
|
310
|
+
self.add_entry_to_caches(entry)
|
|
314
311
|
|
|
315
312
|
def add_tab_to_cache(self, tab: ElnExperimentTab) -> None:
|
|
316
313
|
"""
|
|
@@ -322,7 +319,6 @@ class ExperimentHandler:
|
|
|
322
319
|
"""
|
|
323
320
|
self._tabs.append(tab)
|
|
324
321
|
self._tabs.sort(key=lambda t: t.tab_order)
|
|
325
|
-
self._tabs_by_id[tab.tab_id] = tab
|
|
326
322
|
self._tabs_by_name[tab.tab_name] = tab
|
|
327
323
|
|
|
328
324
|
# FR-46495: Split the creation of the experiment in launch_experiment into a create_experiment function.
|
|
@@ -352,10 +348,22 @@ class ExperimentHandler:
|
|
|
352
348
|
:param active_templates_only: Whether only active templates should be queried for.
|
|
353
349
|
:return: The newly created experiment.
|
|
354
350
|
"""
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
351
|
+
template_query = TemplateExperimentQueryPojo(latest_version_only=(template_version is None),
|
|
352
|
+
active_templates_only=active_templates_only)
|
|
353
|
+
templates: list[ElnTemplate] = context.eln_manager.get_template_experiment_list(template_query)
|
|
354
|
+
launch_template: ElnTemplate | None = None
|
|
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
|
+
|
|
359
367
|
if experiment_name is None:
|
|
360
368
|
experiment_name: str = launch_template.display_name
|
|
361
369
|
if parent_record is not None:
|
|
@@ -582,9 +590,6 @@ class ExperimentHandler:
|
|
|
582
590
|
"""
|
|
583
591
|
Set the experiment's status to Completed. Makes a webservice call to update the experiment. Checks if the
|
|
584
592
|
experiment is already completed, and does nothing if so.
|
|
585
|
-
|
|
586
|
-
NOTE: This will cause the usual process tracking logic to run as if you'd clicked the "Complete Experiment"
|
|
587
|
-
toolbar button. This includes moving the in process samples forward to the next step in the process.
|
|
588
593
|
"""
|
|
589
594
|
if not self.experiment_is_complete():
|
|
590
595
|
self._protocol.complete_protocol()
|
|
@@ -595,11 +600,9 @@ class ExperimentHandler:
|
|
|
595
600
|
Set the experiment's status to Canceled. Makes a webservice call to update the experiment. Checks if the
|
|
596
601
|
experiment is already canceled, and does nothing if so.
|
|
597
602
|
|
|
598
|
-
NOTE: This will
|
|
599
|
-
toolbar button
|
|
600
|
-
|
|
601
|
-
On version 24.12 and earlier, this was not the case, as the process tracking logic was tied to the button
|
|
602
|
-
instead of being on the experiment status change.
|
|
603
|
+
NOTE: This will not run the usual logic around canceling an experiment that you'd see if canceling the
|
|
604
|
+
experiment using the "Cancel Experiment" toolbar button, such as moving in process samples back to the queue,
|
|
605
|
+
as those changes are tied to the button instead of being on the experiment status change.
|
|
603
606
|
"""
|
|
604
607
|
if not self.experiment_is_canceled():
|
|
605
608
|
self._protocol.cancel_protocol()
|
|
@@ -629,14 +632,14 @@ class ExperimentHandler:
|
|
|
629
632
|
"""
|
|
630
633
|
return all([x is not None for x in self.get_steps(step_names, False)])
|
|
631
634
|
|
|
632
|
-
def get_step(self, step_name: str
|
|
635
|
+
def get_step(self, step_name: str, exception_on_none: bool = True) -> ElnEntryStep | None:
|
|
633
636
|
"""
|
|
634
637
|
Get the step of the given name from the experiment.
|
|
635
638
|
|
|
636
639
|
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
637
640
|
list of steps in the experiment and caches them.
|
|
638
641
|
|
|
639
|
-
:param step_name: The name
|
|
642
|
+
:param step_name: The name for the step to return.
|
|
640
643
|
:param exception_on_none: If false, returns None if the entry can't be found. If true, raises an exception
|
|
641
644
|
when the named entry doesn't exist in the experiment.
|
|
642
645
|
:return: An ElnEntrySteps matching the provided name. If there is no match and no exception is to be thrown,
|
|
@@ -644,32 +647,31 @@ class ExperimentHandler:
|
|
|
644
647
|
"""
|
|
645
648
|
return self.get_steps([step_name], exception_on_none)[0]
|
|
646
649
|
|
|
647
|
-
def get_steps(self, step_names: Iterable[str
|
|
650
|
+
def get_steps(self, step_names: Iterable[str], exception_on_none: bool = True) -> list[ElnEntryStep | None]:
|
|
648
651
|
"""
|
|
649
652
|
Get a list of steps of the given names from the experiment, sorted in the same order as the names are provided.
|
|
650
653
|
|
|
651
654
|
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
652
655
|
list of steps in the experiment and caches them.
|
|
653
656
|
|
|
654
|
-
:param step_names: A list of names
|
|
657
|
+
:param step_names: A list of names for the entries to return and the order to return them in.
|
|
655
658
|
:param exception_on_none: If false, returns None for entries that can't be found. If true, raises an exception
|
|
656
659
|
when the named entry doesn't exist in the experiment.
|
|
657
660
|
:return: A list of ElnEntrySteps matching the provided names in the order they were provided in. If there is no
|
|
658
661
|
match for a given step and no exception is to be thrown, returns None for that step.
|
|
659
662
|
"""
|
|
660
663
|
ret_list: list[ElnEntryStep | None] = []
|
|
661
|
-
for
|
|
664
|
+
for name in step_names:
|
|
662
665
|
# If we haven't queried the system for all steps in the experiment yet, then the reason that a step is
|
|
663
666
|
# missing may be because it wasn't in the webhook context. Therefore, query all steps and then check
|
|
664
667
|
# if the step name is still missing from the experiment before potentially throwing an exception.
|
|
665
|
-
if self._queried_all_steps is False and
|
|
666
|
-
self.
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
step: ElnEntryStep = self._steps_by_id.get(step_id)
|
|
668
|
+
if self._queried_all_steps is False and name not in self._steps:
|
|
669
|
+
self._queried_all_steps = True
|
|
670
|
+
self._steps.update({step.get_name(): step for step in self._protocol.get_sorted_step_list()})
|
|
671
|
+
|
|
672
|
+
step: ElnEntryStep = self._steps.get(name)
|
|
671
673
|
if step is None and exception_on_none is True:
|
|
672
|
-
raise SapioException(f"ElnEntryStep of name \"{
|
|
674
|
+
raise SapioException(f"ElnEntryStep of name \"{name}\" not found in experiment with ID {self._exp_id}.")
|
|
673
675
|
ret_list.append(step)
|
|
674
676
|
return ret_list
|
|
675
677
|
|
|
@@ -684,34 +686,14 @@ class ExperimentHandler:
|
|
|
684
686
|
:return: Every entry in the experiment in order of appearance that match the provided data type, if any.
|
|
685
687
|
"""
|
|
686
688
|
if self._queried_all_steps is False:
|
|
687
|
-
self.
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
def sort_steps(step: ElnEntryStep) -> tuple:
|
|
691
|
-
entry = step.eln_entry
|
|
692
|
-
tab_order: int = self.get_tab_for_step(step).tab_order
|
|
693
|
-
entry_order: int = entry.order
|
|
694
|
-
column_order: int = entry.column_order
|
|
695
|
-
return tab_order, entry_order, column_order
|
|
696
|
-
|
|
697
|
-
self._steps.sort(key=sort_steps)
|
|
698
|
-
all_steps: list[ElnEntryStep] = self._steps
|
|
689
|
+
self._queried_all_steps = True
|
|
690
|
+
self._steps.update({step.get_name(): step for step in self._protocol.get_sorted_step_list()})
|
|
691
|
+
all_steps: list[ElnEntryStep] = self._protocol.get_sorted_step_list()
|
|
699
692
|
if data_type is None:
|
|
700
693
|
return all_steps
|
|
701
694
|
data_type: str = AliasUtil.to_data_type_name(data_type)
|
|
702
695
|
return [x for x in all_steps if data_type in x.get_data_type_names()]
|
|
703
696
|
|
|
704
|
-
def _query_all_steps(self) -> None:
|
|
705
|
-
"""
|
|
706
|
-
Query the system for every step in the experiment and cache them.
|
|
707
|
-
"""
|
|
708
|
-
self._queried_all_steps = True
|
|
709
|
-
self._protocol.invalidate()
|
|
710
|
-
self._steps = self._protocol.get_sorted_step_list()
|
|
711
|
-
for step in self._steps:
|
|
712
|
-
self._steps_by_name[step.get_name()] = step
|
|
713
|
-
self._steps_by_id[step.get_id()] = step
|
|
714
|
-
|
|
715
697
|
def get_step_by_option(self, key: str, value: str | None = None) -> ElnEntryStep:
|
|
716
698
|
"""
|
|
717
699
|
Retrieve the step in this experiment that contains an entry option with the provided key and value.
|
|
@@ -800,9 +782,8 @@ class ExperimentHandler:
|
|
|
800
782
|
return
|
|
801
783
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
802
784
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
803
|
-
raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types.
|
|
804
|
-
f"
|
|
805
|
-
f"with the matching data type name.")
|
|
785
|
+
raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. "
|
|
786
|
+
f"Use add_eln_rows or add_sample_details instead.")
|
|
806
787
|
if dt != step.get_data_type_names()[0]:
|
|
807
788
|
raise SapioException(f"Cannot add {dt} records to entry {step.get_name()} of type "
|
|
808
789
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -828,9 +809,8 @@ class ExperimentHandler:
|
|
|
828
809
|
return
|
|
829
810
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
830
811
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
return
|
|
812
|
+
raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. "
|
|
813
|
+
f"Use remove_eln_rows or remove_sample_details instead.")
|
|
834
814
|
if dt != step.get_data_type_names()[0]:
|
|
835
815
|
raise SapioException(f"Cannot remove {dt} records from entry {step.get_name()} of type "
|
|
836
816
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -859,14 +839,9 @@ class ExperimentHandler:
|
|
|
859
839
|
step = self.__to_eln_step(step)
|
|
860
840
|
if records:
|
|
861
841
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
862
|
-
# CR-47532: Add set_step_records support for Experiment Detail and Sample Detail entries.
|
|
863
842
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
if record not in records:
|
|
867
|
-
remove_rows.append(record)
|
|
868
|
-
self.remove_eln_rows(step, remove_rows)
|
|
869
|
-
return
|
|
843
|
+
raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. "
|
|
844
|
+
f"Use add_eln_rows or add_sample_details instead.")
|
|
870
845
|
if dt != step.get_data_type_names()[0]:
|
|
871
846
|
raise SapioException(f"Cannot set {dt} records for entry {step.get_name()} of type "
|
|
872
847
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -893,23 +868,6 @@ class ExperimentHandler:
|
|
|
893
868
|
step.eln_entry.record_id = AliasUtil.to_data_record(record).record_id
|
|
894
869
|
|
|
895
870
|
# FR-46496 - Provide functions for adding and removing rows from an ELN data type entry.
|
|
896
|
-
def add_eln_row(self, step: Step, wrapper_type: type[WrappedType] | None = None) -> WrappedType | PyRecordModel:
|
|
897
|
-
"""
|
|
898
|
-
Add a row to an ELNExperimentDetail or ELNSampleDetail table entry. The row will not appear in the system
|
|
899
|
-
until a record manager store and commit has occurred.
|
|
900
|
-
|
|
901
|
-
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
902
|
-
list of steps in the experiment and caches them.
|
|
903
|
-
|
|
904
|
-
:param step:
|
|
905
|
-
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
906
|
-
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
907
|
-
:param wrapper_type: Optionally wrap the ELN data type in a record model wrapper. If not provided, returns
|
|
908
|
-
an unwrapped PyRecordModel.
|
|
909
|
-
:return: The newly created row.
|
|
910
|
-
"""
|
|
911
|
-
return self.add_eln_rows(step, 1, wrapper_type)[0]
|
|
912
|
-
|
|
913
871
|
def add_eln_rows(self, step: Step, count: int, wrapper_type: type[WrappedType] | None = None) \
|
|
914
872
|
-> list[WrappedType] | list[PyRecordModel]:
|
|
915
873
|
"""
|
|
@@ -938,64 +896,10 @@ class ExperimentHandler:
|
|
|
938
896
|
return self._inst_man.wrap_list(records, wrapper_type)
|
|
939
897
|
return records
|
|
940
898
|
|
|
941
|
-
def
|
|
942
|
-
wrapper_type: type[WrappedType] | None = None) \
|
|
943
|
-
-> WrappedType | PyRecordModel:
|
|
944
|
-
"""
|
|
945
|
-
Add a sample detail to a sample detail entry while relating it to the input sample record.
|
|
946
|
-
|
|
947
|
-
:param step:
|
|
948
|
-
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
949
|
-
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
950
|
-
:param sample: The sample record to add the sample detail to.
|
|
951
|
-
:param wrapper_type: Optionally wrap the sample detail in a record model wrapper. If not provided, returns
|
|
952
|
-
an unwrapped PyRecordModel.
|
|
953
|
-
:return: The newly created sample detail.
|
|
954
|
-
"""
|
|
955
|
-
return self.add_sample_details(step, [sample], wrapper_type)[0]
|
|
956
|
-
|
|
957
|
-
def add_sample_details(self, step: Step, samples: Iterable[RecordModel],
|
|
958
|
-
wrapper_type: type[WrappedType] | None = None) \
|
|
959
|
-
-> list[WrappedType] | list[PyRecordModel]:
|
|
960
|
-
"""
|
|
961
|
-
Add sample details to a sample details entry while relating them to the input sample records.
|
|
962
|
-
|
|
963
|
-
:param step:
|
|
964
|
-
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
965
|
-
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
966
|
-
:param samples: The sample records to add the sample details to.
|
|
967
|
-
:param wrapper_type: Optionally wrap the sample details in a record model wrapper. If not provided, returns
|
|
968
|
-
an unwrapped PyRecordModel.
|
|
969
|
-
:return: The newly created sample details. The indices of the samples in the input list match the index of the
|
|
970
|
-
sample details in this list that they are related to.
|
|
971
|
-
"""
|
|
972
|
-
step = self.__to_eln_step(step)
|
|
973
|
-
if step.eln_entry.entry_type != ElnEntryType.Table:
|
|
974
|
-
raise SapioException("The provided step is not a table entry.")
|
|
975
|
-
dt: str = step.get_data_type_names()[0]
|
|
976
|
-
if not ElnBaseDataType.is_eln_type(dt) or ElnBaseDataType.get_base_type(dt) != ElnBaseDataType.SAMPLE_DETAIL:
|
|
977
|
-
raise SapioException("The provided step is not an ELNSampleDetail entry.")
|
|
978
|
-
records: list[PyRecordModel] = []
|
|
979
|
-
for sample in samples:
|
|
980
|
-
if sample.data_type_name != "Sample":
|
|
981
|
-
raise SapioException(f"Received a {sample.data_type_name} record when Sample records were expected.")
|
|
982
|
-
detail: PyRecordModel = sample.add(Child.create_by_name(dt))
|
|
983
|
-
detail.set_field_values({
|
|
984
|
-
"SampleId": sample.get_field_value("SampleId"),
|
|
985
|
-
"OtherSampleId": sample.get_field_value("OtherSampleId")
|
|
986
|
-
})
|
|
987
|
-
records.append(detail)
|
|
988
|
-
if wrapper_type:
|
|
989
|
-
return self._inst_man.wrap_list(records, wrapper_type)
|
|
990
|
-
return records
|
|
991
|
-
|
|
992
|
-
def remove_eln_row(self, step: Step, record: SapioRecord) -> None:
|
|
899
|
+
def add_eln_row(self, step: Step, wrapper_type: type[WrappedType] | None = None) -> WrappedType | PyRecordModel:
|
|
993
900
|
"""
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
table entry is equivalent to deleting the records for the rows.
|
|
997
|
-
|
|
998
|
-
The row will not be deleted in the system until a record manager store and commit has occurred.
|
|
901
|
+
Add a row to an ELNExperimentDetail or ELNSampleDetail table entry. The row will not appear in the system
|
|
902
|
+
until a record manager store and commit has occurred.
|
|
999
903
|
|
|
1000
904
|
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1001
905
|
list of steps in the experiment and caches them.
|
|
@@ -1003,13 +907,13 @@ class ExperimentHandler:
|
|
|
1003
907
|
:param step:
|
|
1004
908
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1005
909
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1006
|
-
:param record
|
|
1007
|
-
|
|
1008
|
-
|
|
910
|
+
:param wrapper_type: Optionally wrap the ELN data type in a record model wrapper. If not provided, returns
|
|
911
|
+
an unwrapped PyRecordModel.
|
|
912
|
+
:return: The newly created row.
|
|
1009
913
|
"""
|
|
1010
|
-
self.
|
|
914
|
+
return self.add_eln_rows(step, 1, wrapper_type)[0]
|
|
1011
915
|
|
|
1012
|
-
def remove_eln_rows(self, step: Step, records:
|
|
916
|
+
def remove_eln_rows(self, step: Step, records: list[SapioRecord]) -> None:
|
|
1013
917
|
"""
|
|
1014
918
|
Remove rows from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
|
|
1015
919
|
records in the system that match the entry's data type. This means that removing rows from an ELN data type
|
|
@@ -1050,6 +954,61 @@ class ExperimentHandler:
|
|
|
1050
954
|
for record in record_models:
|
|
1051
955
|
record.delete()
|
|
1052
956
|
|
|
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
|
+
|
|
1053
1012
|
# noinspection PyPep8Naming
|
|
1054
1013
|
def update_step(self, step: Step,
|
|
1055
1014
|
entry_name: str | None = None,
|
|
@@ -1117,45 +1076,128 @@ class ExperimentHandler:
|
|
|
1117
1076
|
If you wish to add options to the existing map of options that an entry has, use the
|
|
1118
1077
|
add_step_options method.
|
|
1119
1078
|
"""
|
|
1120
|
-
# FR-47468: Deprecating this since the
|
|
1121
|
-
|
|
1079
|
+
# FR-47468: Deprecating this since the parameters are ordered. The new method requires keyword parameters, so
|
|
1080
|
+
# that we can add new parameters wherever we want without breaking existing code.
|
|
1081
|
+
warnings.warn("Update step is deprecated. Use force_entry_update_params instead.",
|
|
1122
1082
|
DeprecationWarning)
|
|
1123
|
-
|
|
1124
|
-
|
|
1083
|
+
self.force_step_update_params(step,
|
|
1084
|
+
entry_name=entry_name,
|
|
1085
|
+
related_entry_set=related_entry_set,
|
|
1086
|
+
dependency_set=dependency_set,
|
|
1087
|
+
entry_status=entry_status,
|
|
1088
|
+
order=order,
|
|
1089
|
+
description=description,
|
|
1090
|
+
requires_grabber_plugin=requires_grabber_plugin,
|
|
1091
|
+
is_initialization_required=is_initialization_required,
|
|
1092
|
+
notebook_experiment_tab_id=notebook_experiment_tab_id,
|
|
1093
|
+
entry_height=entry_height,
|
|
1094
|
+
column_order=column_order,
|
|
1095
|
+
column_span=column_span,
|
|
1096
|
+
is_removable=is_removable,
|
|
1097
|
+
is_renamable=is_renamable,
|
|
1098
|
+
source_entry_id=source_entry_id,
|
|
1099
|
+
clear_source_entry_id=clear_source_entry_id,
|
|
1100
|
+
is_hidden=is_hidden,
|
|
1101
|
+
is_static_view=is_static_View,
|
|
1102
|
+
is_shown_in_template=is_shown_in_template,
|
|
1103
|
+
template_item_fulfilled_timestamp=template_item_fulfilled_timestamp,
|
|
1104
|
+
clear_template_item_fulfilled_timestamp=clear_template_item_fulfilled_timestamp,
|
|
1105
|
+
entry_options_map=entry_options_map)
|
|
1125
1106
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1107
|
+
# FR-47468: Some functions that can help with entry updates.
|
|
1108
|
+
def force_step_update_params(self, step: Step, *,
|
|
1109
|
+
entry_name: str | None = None,
|
|
1110
|
+
related_entry_set: Iterable[int] | None = None,
|
|
1111
|
+
dependency_set: Iterable[int] | None = None,
|
|
1112
|
+
entry_status: ExperimentEntryStatus | None = None,
|
|
1113
|
+
order: int | None = None,
|
|
1114
|
+
description: str | None = None,
|
|
1115
|
+
requires_grabber_plugin: bool | None = None,
|
|
1116
|
+
is_initialization_required: bool | None = None,
|
|
1117
|
+
notebook_experiment_tab_id: int | None = None,
|
|
1118
|
+
entry_height: int | None = None,
|
|
1119
|
+
column_order: int | None = None,
|
|
1120
|
+
column_span: int | None = None,
|
|
1121
|
+
is_removable: bool | None = None,
|
|
1122
|
+
is_renamable: bool | None = None,
|
|
1123
|
+
source_entry_id: int | None = None,
|
|
1124
|
+
clear_source_entry_id: bool | None = None,
|
|
1125
|
+
is_hidden: bool | None = None,
|
|
1126
|
+
is_static_view: bool | None = None,
|
|
1127
|
+
is_shown_in_template: bool | None = None,
|
|
1128
|
+
template_item_fulfilled_timestamp: int | None = None,
|
|
1129
|
+
clear_template_item_fulfilled_timestamp: bool | None = None,
|
|
1130
|
+
entry_options_map: dict[str, str] | None = None) -> None:
|
|
1131
|
+
"""
|
|
1132
|
+
Immediately sent an update to an entry in this experiment. All changes will be reflected by the ExperimentEntry
|
|
1133
|
+
of the Step that is being updated.
|
|
1132
1134
|
|
|
1133
|
-
update
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
update.order = order
|
|
1138
|
-
update.description = description
|
|
1139
|
-
update.requires_grabber_plugin = requires_grabber_plugin
|
|
1140
|
-
update.is_initialization_required = is_initialization_required
|
|
1141
|
-
update.notebook_experiment_tab_id = notebook_experiment_tab_id
|
|
1142
|
-
update.entry_height = entry_height
|
|
1143
|
-
update.column_order = column_order
|
|
1144
|
-
update.column_span = column_span
|
|
1145
|
-
update.is_removable = is_removable
|
|
1146
|
-
update.is_renamable = is_renamable
|
|
1147
|
-
update.source_entry_id = source_entry_id
|
|
1148
|
-
update.clear_source_entry_id = clear_source_entry_id
|
|
1149
|
-
update.is_hidden = is_hidden
|
|
1150
|
-
update.is_static_View = is_static_View
|
|
1151
|
-
update.is_shown_in_template = is_shown_in_template
|
|
1152
|
-
update.template_item_fulfilled_timestamp = template_item_fulfilled_timestamp
|
|
1153
|
-
update.clear_template_item_fulfilled_timestamp = clear_template_item_fulfilled_timestamp
|
|
1154
|
-
update.entry_options_map = entry_options_map
|
|
1135
|
+
Consider using store_step_update and commit_step_updates instead if the update does not need to be immediate.
|
|
1136
|
+
|
|
1137
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1138
|
+
list of steps in the experiment and caches them.
|
|
1155
1139
|
|
|
1140
|
+
:param step:
|
|
1141
|
+
The entry step to update.
|
|
1142
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1143
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1144
|
+
:param entry_name: The new name of this entry.
|
|
1145
|
+
:param related_entry_set: The new set of entry IDs for the entries that are related (implicitly dependent) to
|
|
1146
|
+
this entry. Completely overwrites the existing related entries.
|
|
1147
|
+
:param dependency_set: The new set of entry IDs for the entries that are dependent (explicitly dependent) on
|
|
1148
|
+
this entry. Completely overwrites the existing dependent entries.
|
|
1149
|
+
:param entry_status: The new status of this entry.
|
|
1150
|
+
:param order: The row order of this entry in its tab.
|
|
1151
|
+
:param description: The new description of this entry.
|
|
1152
|
+
:param requires_grabber_plugin: Whether this entry's initialization is handled by a grabber plugin. If true,
|
|
1153
|
+
then is_initialization_required is forced to true by the server.
|
|
1154
|
+
:param is_initialization_required: Whether the user is required to manually initialize this entry.
|
|
1155
|
+
:param notebook_experiment_tab_id: The ID of the tab that this entry should appear on.
|
|
1156
|
+
:param entry_height: The height of this entry.
|
|
1157
|
+
:param column_order: The column order of this entry.
|
|
1158
|
+
:param column_span: How many columns this entry spans.
|
|
1159
|
+
:param is_removable: Whether this entry can be removed by the user.
|
|
1160
|
+
:param is_renamable: Whether this entry can be renamed by the user.
|
|
1161
|
+
:param source_entry_id: The ID of this entry from its template.
|
|
1162
|
+
:param clear_source_entry_id: True if the source entry ID should be cleared.
|
|
1163
|
+
:param is_hidden: Whether this entry is hidden from the user.
|
|
1164
|
+
:param is_static_view: Whether this entry is static. Static entries are uneditable and shared across all
|
|
1165
|
+
experiments of the same template.
|
|
1166
|
+
:param is_shown_in_template: Whether this entry is saved to and shown in the experiment's template.
|
|
1167
|
+
:param template_item_fulfilled_timestamp: A timestamp in milliseconds for when this entry was initialized.
|
|
1168
|
+
:param clear_template_item_fulfilled_timestamp: True if the template item fulfilled timestamp should be cleared,
|
|
1169
|
+
uninitializing the entry.
|
|
1170
|
+
:param entry_options_map:
|
|
1171
|
+
The new map of options for this entry. Completely overwrites the existing options map.
|
|
1172
|
+
Any changes to the entry options will update this ExperimentHandler's cache of entry options.
|
|
1173
|
+
If you wish to add options to the existing map of options that an entry has, use the
|
|
1174
|
+
add_step_options method.
|
|
1175
|
+
"""
|
|
1176
|
+
update = self._criteria_from_params(step,
|
|
1177
|
+
entry_name=entry_name,
|
|
1178
|
+
related_entry_set=related_entry_set,
|
|
1179
|
+
dependency_set=dependency_set,
|
|
1180
|
+
entry_status=entry_status,
|
|
1181
|
+
order=order,
|
|
1182
|
+
description=description,
|
|
1183
|
+
requires_grabber_plugin=requires_grabber_plugin,
|
|
1184
|
+
is_initialization_required=is_initialization_required,
|
|
1185
|
+
notebook_experiment_tab_id=notebook_experiment_tab_id,
|
|
1186
|
+
entry_height=entry_height,
|
|
1187
|
+
column_order=column_order,
|
|
1188
|
+
column_span=column_span,
|
|
1189
|
+
is_removable=is_removable,
|
|
1190
|
+
is_renamable=is_renamable,
|
|
1191
|
+
source_entry_id=source_entry_id,
|
|
1192
|
+
clear_source_entry_id=clear_source_entry_id,
|
|
1193
|
+
is_hidden=is_hidden,
|
|
1194
|
+
is_static_view=is_static_view,
|
|
1195
|
+
is_shown_in_template=is_shown_in_template,
|
|
1196
|
+
template_item_fulfilled_timestamp=template_item_fulfilled_timestamp,
|
|
1197
|
+
clear_template_item_fulfilled_timestamp=clear_template_item_fulfilled_timestamp,
|
|
1198
|
+
entry_options_map=entry_options_map)
|
|
1156
1199
|
self.force_step_update(step, update)
|
|
1157
1200
|
|
|
1158
|
-
# FR-47468: Some functions that can help with entry updates.
|
|
1159
1201
|
def force_step_update(self, step: Step, update: AbstractElnEntryUpdateCriteria) -> None:
|
|
1160
1202
|
"""
|
|
1161
1203
|
Immediately sent an update to an entry in this experiment. All changes will be reflected by the ExperimentEntry
|
|
@@ -1227,6 +1269,67 @@ class ExperimentHandler:
|
|
|
1227
1269
|
self._update_entry_details(self._steps_by_id[step_id], criteria)
|
|
1228
1270
|
self._step_updates.clear()
|
|
1229
1271
|
|
|
1272
|
+
def _criteria_from_params(self, step: Step, *,
|
|
1273
|
+
entry_name: str | None = None,
|
|
1274
|
+
related_entry_set: Iterable[int] | None = None,
|
|
1275
|
+
dependency_set: Iterable[int] | None = None,
|
|
1276
|
+
entry_status: ExperimentEntryStatus | None = None,
|
|
1277
|
+
order: int | None = None,
|
|
1278
|
+
description: str | None = None,
|
|
1279
|
+
requires_grabber_plugin: bool | None = None,
|
|
1280
|
+
is_initialization_required: bool | None = None,
|
|
1281
|
+
notebook_experiment_tab_id: int | None = None,
|
|
1282
|
+
entry_height: int | None = None,
|
|
1283
|
+
column_order: int | None = None,
|
|
1284
|
+
column_span: int | None = None,
|
|
1285
|
+
is_removable: bool | None = None,
|
|
1286
|
+
is_renamable: bool | None = None,
|
|
1287
|
+
source_entry_id: int | None = None,
|
|
1288
|
+
clear_source_entry_id: bool | None = None,
|
|
1289
|
+
is_hidden: bool | None = None,
|
|
1290
|
+
is_static_view: bool | None = None,
|
|
1291
|
+
is_shown_in_template: bool | None = None,
|
|
1292
|
+
template_item_fulfilled_timestamp: int | None = None,
|
|
1293
|
+
clear_template_item_fulfilled_timestamp: bool | None = None,
|
|
1294
|
+
entry_options_map: dict[str, str] | None = None) -> AbstractElnEntryUpdateCriteria:
|
|
1295
|
+
"""
|
|
1296
|
+
Create an abstract update criteria object from the provided parameters for the given step.
|
|
1297
|
+
"""
|
|
1298
|
+
step: ElnEntryStep = self.__to_eln_step(step)
|
|
1299
|
+
update = AbstractElnEntryUpdateCriteria(step.eln_entry.entry_type)
|
|
1300
|
+
|
|
1301
|
+
# These two variables could be iterables that aren't lists. Convert them to plain
|
|
1302
|
+
# lists, since that's what the update criteria is expecting.
|
|
1303
|
+
if related_entry_set is not None:
|
|
1304
|
+
related_entry_set = list(related_entry_set)
|
|
1305
|
+
if dependency_set is not None:
|
|
1306
|
+
dependency_set = list(dependency_set)
|
|
1307
|
+
|
|
1308
|
+
update.entry_name = entry_name
|
|
1309
|
+
update.related_entry_set = related_entry_set
|
|
1310
|
+
update.dependency_set = dependency_set
|
|
1311
|
+
update.entry_status = entry_status
|
|
1312
|
+
update.order = order
|
|
1313
|
+
update.description = description
|
|
1314
|
+
update.requires_grabber_plugin = requires_grabber_plugin
|
|
1315
|
+
update.is_initialization_required = is_initialization_required
|
|
1316
|
+
update.notebook_experiment_tab_id = notebook_experiment_tab_id
|
|
1317
|
+
update.entry_height = entry_height
|
|
1318
|
+
update.column_order = column_order
|
|
1319
|
+
update.column_span = column_span
|
|
1320
|
+
update.is_removable = is_removable
|
|
1321
|
+
update.is_renamable = is_renamable
|
|
1322
|
+
update.source_entry_id = source_entry_id
|
|
1323
|
+
update.clear_source_entry_id = clear_source_entry_id
|
|
1324
|
+
update.is_hidden = is_hidden
|
|
1325
|
+
update.is_static_View = is_static_view
|
|
1326
|
+
update.is_shown_in_template = is_shown_in_template
|
|
1327
|
+
update.template_item_fulfilled_timestamp = template_item_fulfilled_timestamp
|
|
1328
|
+
update.clear_template_item_fulfilled_timestamp = clear_template_item_fulfilled_timestamp
|
|
1329
|
+
update.entry_options_map = entry_options_map
|
|
1330
|
+
|
|
1331
|
+
return update
|
|
1332
|
+
|
|
1230
1333
|
@staticmethod
|
|
1231
1334
|
def _merge_updates(new_update: AbstractElnEntryUpdateCriteria, old_update: AbstractElnEntryUpdateCriteria) -> None:
|
|
1232
1335
|
"""
|
|
@@ -1244,10 +1347,10 @@ class ExperimentHandler:
|
|
|
1244
1347
|
entry: ExperimentEntry = step.eln_entry
|
|
1245
1348
|
if update.entry_name is not None:
|
|
1246
1349
|
# PR-46477 - Ensure that the previous name of the updated entry already existed in the cache.
|
|
1247
|
-
if entry.entry_name in self.
|
|
1248
|
-
self.
|
|
1350
|
+
if entry.entry_name in self._steps:
|
|
1351
|
+
self._steps.pop(entry.entry_name)
|
|
1249
1352
|
entry.entry_name = update.entry_name
|
|
1250
|
-
self.
|
|
1353
|
+
self._steps.update({update.entry_name: step})
|
|
1251
1354
|
if update.related_entry_set is not None:
|
|
1252
1355
|
entry.related_entry_id_set = update.related_entry_set
|
|
1253
1356
|
if update.dependency_set is not None:
|
|
@@ -1411,14 +1514,12 @@ class ExperimentHandler:
|
|
|
1411
1514
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1412
1515
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1413
1516
|
:param mapping: The new options and values to add to the existing step options, provided as some Mapping
|
|
1414
|
-
(e.g. a
|
|
1415
|
-
|
|
1517
|
+
(e.g. a Dict). If an option key already exists and is provided in the mapping, overwrites the existing value
|
|
1518
|
+
for that key.
|
|
1416
1519
|
"""
|
|
1417
1520
|
options: dict[str, str] = self.get_step_options(step)
|
|
1418
1521
|
options.update(mapping)
|
|
1419
|
-
|
|
1420
|
-
update.entry_options_map = options
|
|
1421
|
-
self.force_step_update(step, update)
|
|
1522
|
+
self.force_step_update_params(step, entry_options_map=options)
|
|
1422
1523
|
|
|
1423
1524
|
def initialize_step(self, step: Step) -> None:
|
|
1424
1525
|
"""
|
|
@@ -1436,9 +1537,7 @@ class ExperimentHandler:
|
|
|
1436
1537
|
# Avoid unnecessary calls if the step is already initialized.
|
|
1437
1538
|
step: ElnEntryStep = self.__to_eln_step(step)
|
|
1438
1539
|
if step.eln_entry.template_item_fulfilled_timestamp is None:
|
|
1439
|
-
|
|
1440
|
-
update.template_item_fulfilled_timestamp = TimeUtil.now_in_millis()
|
|
1441
|
-
self.force_step_update(step, update)
|
|
1540
|
+
self.force_step_update_params(step, template_item_fulfilled_timestamp=TimeUtil.now_in_millis())
|
|
1442
1541
|
|
|
1443
1542
|
def uninitialize_step(self, step: Step) -> None:
|
|
1444
1543
|
"""
|
|
@@ -1456,9 +1555,7 @@ class ExperimentHandler:
|
|
|
1456
1555
|
# Avoid unnecessary calls if the step is already uninitialized.
|
|
1457
1556
|
step: ElnEntryStep = self.__to_eln_step(step)
|
|
1458
1557
|
if step.eln_entry.template_item_fulfilled_timestamp is not None:
|
|
1459
|
-
|
|
1460
|
-
update.clear_template_item_fulfilled_timestamp = True
|
|
1461
|
-
self.force_step_update(step, update)
|
|
1558
|
+
self.force_step_update_params(step, clear_template_item_fulfilled_timestamp=True)
|
|
1462
1559
|
|
|
1463
1560
|
def complete_step(self, step: Step) -> None:
|
|
1464
1561
|
"""
|
|
@@ -1515,9 +1612,7 @@ class ExperimentHandler:
|
|
|
1515
1612
|
"""
|
|
1516
1613
|
step = self.__to_eln_step(step)
|
|
1517
1614
|
if step.eln_entry.entry_status in self._ENTRY_LOCKED_STATUSES:
|
|
1518
|
-
|
|
1519
|
-
update.entry_status = ExperimentEntryStatus.Disabled
|
|
1520
|
-
self.force_step_update(step, update)
|
|
1615
|
+
self.force_step_update_params(step, entry_status=ExperimentEntryStatus.Disabled)
|
|
1521
1616
|
|
|
1522
1617
|
def step_is_submitted(self, step: Step) -> bool:
|
|
1523
1618
|
"""
|
|
@@ -1561,7 +1656,6 @@ class ExperimentHandler:
|
|
|
1561
1656
|
if not self._queried_all_tabs:
|
|
1562
1657
|
self._tabs = self._eln_man.get_tabs_for_experiment(self._exp_id)
|
|
1563
1658
|
self._tabs.sort(key=lambda t: t.tab_order)
|
|
1564
|
-
self._tabs_by_id = {tab.tab_id: tab for tab in self._tabs}
|
|
1565
1659
|
self._tabs_by_name = {tab.tab_name: tab for tab in self._tabs}
|
|
1566
1660
|
return self._tabs
|
|
1567
1661
|
|
|
@@ -1595,34 +1689,21 @@ class ExperimentHandler:
|
|
|
1595
1689
|
self.add_tab_to_cache(tab)
|
|
1596
1690
|
return tab
|
|
1597
1691
|
|
|
1598
|
-
def get_tab(self,
|
|
1692
|
+
def get_tab(self, tab_name: str) -> ElnExperimentTab:
|
|
1599
1693
|
"""
|
|
1600
1694
|
Return the tab with the input name.
|
|
1601
1695
|
|
|
1602
1696
|
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1603
1697
|
list of tabs in the experiment and caches them.
|
|
1604
1698
|
|
|
1605
|
-
:param
|
|
1606
|
-
:
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
self.get_all_tabs()
|
|
1612
|
-
eln_tab = self._tabs_by_name.get(tab)
|
|
1613
|
-
elif isinstance(tab, int):
|
|
1614
|
-
# The given integer is expected to be 1-indexed, but we read from the list with a 0-index.
|
|
1615
|
-
tab -= 1
|
|
1616
|
-
tabs = self.get_all_tabs()
|
|
1617
|
-
eln_tab = tabs[tab] if len(tabs) > tab else None
|
|
1618
|
-
else:
|
|
1619
|
-
raise SapioException(f"Tab must be a string or an integer, not {type(tab)}.")
|
|
1620
|
-
if eln_tab is None and exception_on_none:
|
|
1621
|
-
raise SapioException(f"No tab with the name\\order \"{tab}\" exists in this experiment.")
|
|
1622
|
-
return eln_tab
|
|
1699
|
+
:param tab_name: The name of the tab to get.
|
|
1700
|
+
:return: The tab with the input name.
|
|
1701
|
+
"""
|
|
1702
|
+
if tab_name not in self._tabs_by_name:
|
|
1703
|
+
self.get_all_tabs()
|
|
1704
|
+
return self._tabs_by_name[tab_name]
|
|
1623
1705
|
|
|
1624
|
-
def get_steps_in_tab(self, tab: Tab, data_type: DataTypeIdentifier | None = None)
|
|
1625
|
-
-> list[ElnEntryStep]:
|
|
1706
|
+
def get_steps_in_tab(self, tab: Tab, data_type: DataTypeIdentifier | None = None) -> list[ElnEntryStep]:
|
|
1626
1707
|
"""
|
|
1627
1708
|
Get all the steps in the input tab sorted in order of appearance.
|
|
1628
1709
|
|
|
@@ -1632,8 +1713,7 @@ class ExperimentHandler:
|
|
|
1632
1713
|
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1633
1714
|
and caches them.
|
|
1634
1715
|
|
|
1635
|
-
:param tab: The tab to get the steps of.
|
|
1636
|
-
The order is 1-indexed.
|
|
1716
|
+
:param tab: The tab or tab name to get the steps of.
|
|
1637
1717
|
:param data_type: The data type to filter the steps by. If None, all steps are returned.
|
|
1638
1718
|
:return: A list of all the steps in the input tab sorted in order of appearance.
|
|
1639
1719
|
"""
|
|
@@ -1642,30 +1722,9 @@ class ExperimentHandler:
|
|
|
1642
1722
|
for step in self.get_all_steps(data_type):
|
|
1643
1723
|
if step.eln_entry.notebook_experiment_tab_id == tab.tab_id:
|
|
1644
1724
|
steps.append(step)
|
|
1725
|
+
steps.sort(key=lambda s: (s.eln_entry.order, s.eln_entry.column_order))
|
|
1645
1726
|
return steps
|
|
1646
1727
|
|
|
1647
|
-
def get_tab_for_step(self, step: Step) -> ElnExperimentTab:
|
|
1648
|
-
"""
|
|
1649
|
-
Get the tab that a particular step is located in.
|
|
1650
|
-
|
|
1651
|
-
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1652
|
-
list of tabs in the experiment and caches them.
|
|
1653
|
-
|
|
1654
|
-
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1655
|
-
and caches them.
|
|
1656
|
-
|
|
1657
|
-
:param step:
|
|
1658
|
-
The step to get the position of.
|
|
1659
|
-
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1660
|
-
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1661
|
-
:return: The tab that the input step is located in.
|
|
1662
|
-
"""
|
|
1663
|
-
step = self.__to_eln_step(step)
|
|
1664
|
-
tab_id = step.eln_entry.notebook_experiment_tab_id
|
|
1665
|
-
if tab_id not in self._tabs_by_id:
|
|
1666
|
-
self.get_all_tabs()
|
|
1667
|
-
return self._tabs_by_id.get(tab_id)
|
|
1668
|
-
|
|
1669
1728
|
def get_next_entry_order_in_tab(self, tab: Tab) -> int:
|
|
1670
1729
|
"""
|
|
1671
1730
|
Get the next available order for a new entry in the input tab.
|
|
@@ -1676,70 +1735,402 @@ class ExperimentHandler:
|
|
|
1676
1735
|
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1677
1736
|
and caches them.
|
|
1678
1737
|
|
|
1679
|
-
:param tab: The tab to get the steps of.
|
|
1680
|
-
The order is 1-indexed.
|
|
1738
|
+
:param tab: The tab or tab name to get the steps of.
|
|
1681
1739
|
:return: The next available order for a new entry in the input tab.
|
|
1682
1740
|
"""
|
|
1683
1741
|
steps = self.get_steps_in_tab(tab)
|
|
1684
1742
|
return steps[-1].eln_entry.order + 1 if steps else 0
|
|
1685
1743
|
|
|
1686
|
-
# FR-
|
|
1687
|
-
def
|
|
1688
|
-
|
|
1689
|
-
|
|
1744
|
+
# FR-47468: Add functions for creating new entries in the experiment.
|
|
1745
|
+
def create_attachment_step(self, entry_name: str, data_type: DataTypeIdentifier,
|
|
1746
|
+
*,
|
|
1747
|
+
position: ElnEntryPosition | None = None,
|
|
1748
|
+
attachments: list[EntryAttachment] | list[SapioRecord] | None = None) -> ElnEntryStep:
|
|
1749
|
+
"""
|
|
1750
|
+
Create a new attachment entry in the experiment.
|
|
1751
|
+
|
|
1752
|
+
:param entry_name: The name of the entry.
|
|
1753
|
+
:param data_type: The data type of the entry.
|
|
1754
|
+
:param position: Information about where to place the entry in the experiment.
|
|
1755
|
+
:param attachments: The list of attachments to initially populate the entry with.
|
|
1756
|
+
:return: The newly created attachment entry.
|
|
1757
|
+
"""
|
|
1758
|
+
step = self._create_step(ElnEntryType.Attachment, entry_name, data_type, position)
|
|
1759
|
+
if attachments:
|
|
1760
|
+
entry_attachments: list[EntryAttachment] = []
|
|
1761
|
+
for entry in attachments:
|
|
1762
|
+
if isinstance(entry, EntryAttachment):
|
|
1763
|
+
entry_attachments.append(entry)
|
|
1764
|
+
elif isinstance(entry, SapioRecord):
|
|
1765
|
+
entry: SapioRecord
|
|
1766
|
+
file_name: str = entry.get_field_value("FilePath")
|
|
1767
|
+
if not file_name:
|
|
1768
|
+
file_name = entry.get_field_value(SystemFields.DATA_RECORD_NAME__FIELD.field_name)
|
|
1769
|
+
rec_id: int = AliasUtil.to_record_id(entry)
|
|
1770
|
+
entry_attachments.append(EntryRecordAttachment(file_name, rec_id))
|
|
1771
|
+
else:
|
|
1772
|
+
raise SapioException("Attachments must be of type EntryAttachment or SapioRecord.")
|
|
1773
|
+
update = ElnAttachmentEntryUpdateCriteria()
|
|
1774
|
+
update.entry_attachment_list = attachments
|
|
1775
|
+
self.force_step_update(step, update)
|
|
1776
|
+
return step
|
|
1690
1777
|
|
|
1691
|
-
|
|
1692
|
-
|
|
1778
|
+
def create_form_step(self, entry_name: str,
|
|
1779
|
+
data_type: DataTypeIdentifier,
|
|
1780
|
+
fields: list[FieldIdentifier] | None = None,
|
|
1781
|
+
layout_name: str | None = None,
|
|
1782
|
+
form_names: list[str] | None = None,
|
|
1783
|
+
*,
|
|
1784
|
+
position: ElnEntryPosition | None = None,
|
|
1785
|
+
record: SapioRecord | None = None) -> ElnEntryStep:
|
|
1786
|
+
"""
|
|
1787
|
+
Create a new form entry in the experiment.
|
|
1788
|
+
|
|
1789
|
+
:param entry_name: The name of the entry.
|
|
1790
|
+
:param data_type: The data type of the entry.
|
|
1791
|
+
:param fields: The list of data field names for the given data type that should appear on the
|
|
1792
|
+
form. Fields will appear in the order they are provided. If not provided and a layout name is not provided,
|
|
1793
|
+
the entry will be created with the data type's default layout.
|
|
1794
|
+
:param layout_name: The name of the layout to use for the entry. If not provided and field names are not
|
|
1795
|
+
provided, the entry will be created with the data type's default layout.
|
|
1796
|
+
:param form_names: The list of layout component form names to use from the specified layout name. If not
|
|
1797
|
+
provided, then all available forms from the specified layout will be used.
|
|
1798
|
+
:param position: Information about where to place the entry in the experiment.
|
|
1799
|
+
:param record: The record to initially populate the entry with.
|
|
1800
|
+
:return: The newly created form entry.
|
|
1801
|
+
"""
|
|
1802
|
+
if record:
|
|
1803
|
+
rdt: str = AliasUtil.to_data_type_name(record)
|
|
1804
|
+
sdt: str = AliasUtil.to_data_type_name(data_type)
|
|
1805
|
+
if rdt != sdt:
|
|
1806
|
+
raise SapioException(f"Cannot set {rdt} records for entry {entry_name} of type "
|
|
1807
|
+
f"{sdt}.")
|
|
1808
|
+
|
|
1809
|
+
step = self._create_step(ElnEntryType.Form, entry_name, data_type, position)
|
|
1810
|
+
if fields or layout_name or form_names or record:
|
|
1811
|
+
update = ElnFormEntryUpdateCriteria()
|
|
1812
|
+
if fields:
|
|
1813
|
+
update.data_field_name_list = AliasUtil.to_data_field_names(fields)
|
|
1814
|
+
update.data_type_layout_name = layout_name
|
|
1815
|
+
update.form_name_list = form_names
|
|
1816
|
+
if record:
|
|
1817
|
+
update.record_id = AliasUtil.to_record_id(record)
|
|
1818
|
+
self.force_step_update(step, update)
|
|
1819
|
+
return step
|
|
1693
1820
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1821
|
+
def create_experiment_detail_form_step(self, entry_name: str,
|
|
1822
|
+
fields: list[ElnDataTypeFields] | None = None,
|
|
1823
|
+
field_map: FieldMap | None = None,
|
|
1824
|
+
*,
|
|
1825
|
+
position: ElnEntryPosition | None = None,
|
|
1826
|
+
is_field_addable: bool | None = None,
|
|
1827
|
+
is_existing_field_removable: bool | None = None) -> ElnEntryStep:
|
|
1828
|
+
"""
|
|
1829
|
+
Create a new ELN experiment details form entry in the experiment.
|
|
1830
|
+
|
|
1831
|
+
:param entry_name: The name of the entry.
|
|
1832
|
+
:param fields: A list of objects representing the data fields that should appear on the entry. Fields
|
|
1833
|
+
will appear in the order they are provided.
|
|
1834
|
+
:param field_map: A field map that will be used to populate the entry. The data field names in
|
|
1835
|
+
the map must match the field names of the provided field definitions.
|
|
1836
|
+
:param position: Information about where to place the entry in the experiment.
|
|
1837
|
+
:param is_field_addable: Whether users are able to add additional fields to this entry in the UI.
|
|
1838
|
+
:param is_existing_field_removable: Whether users are able to remove existing fields from this entry in the UI.
|
|
1839
|
+
:return: The newly created form entry.
|
|
1840
|
+
"""
|
|
1841
|
+
return self._create_eln_dt_form_step(entry_name, ElnBaseDataType.EXPERIMENT_DETAIL, fields, field_map,
|
|
1842
|
+
position=position,
|
|
1843
|
+
is_field_addable=is_field_addable,
|
|
1844
|
+
is_existing_field_removable=is_existing_field_removable)
|
|
1845
|
+
|
|
1846
|
+
def create_sample_detail_form_step(self, entry_name: str,
|
|
1847
|
+
fields: list[ElnDataTypeFields] | None = None,
|
|
1848
|
+
field_map: FieldMap | None = None,
|
|
1849
|
+
*,
|
|
1850
|
+
position: ElnEntryPosition | None = None,
|
|
1851
|
+
is_field_addable: bool | None = None,
|
|
1852
|
+
is_existing_field_removable: bool | None = None) -> ElnEntryStep:
|
|
1853
|
+
"""
|
|
1854
|
+
Create a new ELN sample details form entry in the experiment.
|
|
1855
|
+
|
|
1856
|
+
:param entry_name: The name of the entry.
|
|
1857
|
+
:param fields: A list of objects representing the data fields that should appear on the entry. Fields
|
|
1858
|
+
will appear in the order they are provided.
|
|
1859
|
+
:param field_map: A field map that will be used to populate the entry. The data field names in
|
|
1860
|
+
the map must match the field names of the provided field definitions.
|
|
1861
|
+
:param position: Information about where to place the entry in the experiment.
|
|
1862
|
+
:param is_field_addable: Whether users are able to add additional fields to this entry in the UI.
|
|
1863
|
+
:param is_existing_field_removable: Whether users are able to remove existing fields from this entry in the UI.
|
|
1864
|
+
:return: The newly created form entry.
|
|
1865
|
+
"""
|
|
1866
|
+
return self._create_eln_dt_form_step(entry_name, ElnBaseDataType.SAMPLE_DETAIL, fields, field_map,
|
|
1867
|
+
position=position,
|
|
1868
|
+
is_field_addable=is_field_addable,
|
|
1869
|
+
is_existing_field_removable=is_existing_field_removable)
|
|
1870
|
+
|
|
1871
|
+
def _create_eln_dt_form_step(self, entry_name: str,
|
|
1872
|
+
dt: ElnBaseDataType,
|
|
1873
|
+
fields: list[ElnDataTypeFields] | None = None,
|
|
1874
|
+
field_map: FieldMap | None = None,
|
|
1875
|
+
*,
|
|
1876
|
+
position: ElnEntryPosition | None = None,
|
|
1877
|
+
is_field_addable: bool | None = None,
|
|
1878
|
+
is_existing_field_removable: bool | None = None) -> ElnEntryStep:
|
|
1879
|
+
fields: list[AbstractVeloxFieldDefinition] | None = self._to_field_defs(fields, dt)
|
|
1880
|
+
step = self._create_step(ElnEntryType.Form, entry_name, dt.data_type_name,
|
|
1881
|
+
position,
|
|
1882
|
+
field_definition_list=fields,
|
|
1883
|
+
field_map_list=[field_map] if field_map else None)
|
|
1884
|
+
if is_field_addable is not None or is_existing_field_removable is not None:
|
|
1885
|
+
update = ElnFormEntryUpdateCriteria()
|
|
1886
|
+
update.is_field_addable = is_field_addable
|
|
1887
|
+
update.is_existing_field_removable = is_existing_field_removable
|
|
1888
|
+
self.force_step_update(step, update)
|
|
1889
|
+
return step
|
|
1890
|
+
|
|
1891
|
+
def create_plugin_step(self, entry_name: str, data_type: DataTypeIdentifier, plugin_path: str, *,
|
|
1892
|
+
position: ElnEntryPosition | None = None) -> ElnEntryStep:
|
|
1893
|
+
"""
|
|
1894
|
+
Create a new plugin entry in the experiment.
|
|
1895
|
+
|
|
1896
|
+
:param entry_name: The name of the entry.
|
|
1897
|
+
:param data_type: The data type of the entry.
|
|
1898
|
+
:param plugin_path: The plugin path to the CSP that determines this entry's functionality.
|
|
1899
|
+
:param position: Information about where to place the entry in the experiment.
|
|
1900
|
+
:return: The newly created plugin entry.
|
|
1901
|
+
"""
|
|
1902
|
+
return self._create_step(ElnEntryType.Plugin, entry_name, data_type, position,
|
|
1903
|
+
csp_plugin_name=plugin_path)
|
|
1904
|
+
|
|
1905
|
+
def create_table_step(self, entry_name: str, data_type: DataTypeIdentifier,
|
|
1906
|
+
fields: list[FieldIdentifier] | list[TableColumn] | None = None,
|
|
1907
|
+
layout_name: str | None = None,
|
|
1908
|
+
show_key_fields: bool | None = None,
|
|
1909
|
+
*,
|
|
1910
|
+
position: ElnEntryPosition | None = None,
|
|
1911
|
+
records: list[SapioRecord] | None = None) -> ElnEntryStep:
|
|
1912
|
+
"""
|
|
1913
|
+
Create a new table entry in the experiment.
|
|
1914
|
+
|
|
1915
|
+
:param entry_name: The name of the entry.
|
|
1916
|
+
:param data_type: The data type of the entry.
|
|
1917
|
+
:param fields: The list of data field names for the given data type that should appear on the
|
|
1918
|
+
table. You may also provide a list of TableColumns instead of a list of field names for when you want to
|
|
1919
|
+
set the sort order and direction of the columns in the table. Fields will appear in the order they are
|
|
1920
|
+
provided. If not provided and a layout name is not provided, the entry will be created with the data type's
|
|
1921
|
+
default layout.
|
|
1922
|
+
:param layout_name: The name of the layout to use for the entry. If not provided and field names are not
|
|
1923
|
+
provided, the entry will be created with the data type's default layout.
|
|
1924
|
+
:param show_key_fields: Whether the table should only show key fields of the data type.
|
|
1925
|
+
:param position: Information about where to place the entry in the experiment.
|
|
1926
|
+
:param records: The list of records to initially populate the entry with.
|
|
1927
|
+
:return: The newly created table entry.
|
|
1928
|
+
"""
|
|
1929
|
+
step = self._create_step(ElnEntryType.Table, entry_name, data_type, position)
|
|
1930
|
+
if fields or layout_name:
|
|
1931
|
+
update = ElnTableEntryUpdateCriteria()
|
|
1932
|
+
if fields:
|
|
1933
|
+
dt: str = AliasUtil.to_data_type_name(data_type)
|
|
1934
|
+
columns: list[TableColumn] = []
|
|
1935
|
+
for field in fields:
|
|
1936
|
+
if isinstance(field, TableColumn):
|
|
1937
|
+
columns.append(field)
|
|
1938
|
+
else:
|
|
1939
|
+
columns.append(TableColumn(dt, AliasUtil.to_data_field_name(field)))
|
|
1940
|
+
update.table_column_list = columns
|
|
1941
|
+
update.data_type_layout_name = layout_name
|
|
1942
|
+
update.show_key_fields = show_key_fields
|
|
1943
|
+
self.force_step_update(step, update)
|
|
1944
|
+
if records:
|
|
1945
|
+
self.set_step_records(step, records)
|
|
1946
|
+
return step
|
|
1947
|
+
|
|
1948
|
+
def create_experiment_detail_table_step(self, entry_name: str,
|
|
1949
|
+
fields: list[ElnDataTypeFields] | None = None,
|
|
1950
|
+
field_maps: list[FieldMap] | None = None, *,
|
|
1951
|
+
position: ElnEntryPosition | None = None,
|
|
1952
|
+
is_field_addable: bool | None = None,
|
|
1953
|
+
is_existing_field_removable: bool | None = None) -> ElnEntryStep:
|
|
1954
|
+
"""
|
|
1955
|
+
Create a new ELN experiment details table entry in the experiment.
|
|
1956
|
+
|
|
1957
|
+
:param entry_name: The name of the entry.
|
|
1958
|
+
:param fields: A list of objects representing the data fields that should appear on the entry. Fields
|
|
1959
|
+
will appear in the order they are provided.
|
|
1960
|
+
:param field_maps: A field maps list that will be used to populate the entry. The data field names in
|
|
1961
|
+
the maps must match the field names of the provided field definitions.
|
|
1962
|
+
:param position: Information about where to place the entry in the experiment.
|
|
1963
|
+
:param is_field_addable: Whether users are able to add additional fields to this entry in the UI.
|
|
1964
|
+
:param is_existing_field_removable: Whether users are able to remove existing fields from this entry in the UI.
|
|
1965
|
+
:return: The newly created table entry.
|
|
1966
|
+
"""
|
|
1967
|
+
return self._create_eln_dt_table_step(entry_name, ElnBaseDataType.EXPERIMENT_DETAIL, fields, field_maps,
|
|
1968
|
+
position=position,
|
|
1969
|
+
is_field_addable=is_field_addable,
|
|
1970
|
+
is_existing_field_removable=is_existing_field_removable)
|
|
1971
|
+
|
|
1972
|
+
def create_sample_detail_table_step(self, entry_name: str,
|
|
1973
|
+
fields: list[ElnDataTypeFields] | None = None,
|
|
1974
|
+
field_maps: list[FieldMap] | None = None, *,
|
|
1975
|
+
position: ElnEntryPosition | None = None,
|
|
1976
|
+
is_field_addable: bool | None = None,
|
|
1977
|
+
is_existing_field_removable: bool | None = None) -> ElnEntryStep:
|
|
1978
|
+
"""
|
|
1979
|
+
Create a new ELN sample details table entry in the experiment.
|
|
1980
|
+
|
|
1981
|
+
:param entry_name: The name of the entry.
|
|
1982
|
+
:param fields: A list of objects representing the data fields that should appear on the entry. Fields
|
|
1983
|
+
will appear in the order they are provided.
|
|
1984
|
+
:param field_maps: A field maps list that will be used to populate the entry. The data field names in
|
|
1985
|
+
the maps must match the field names of the provided field definitions.
|
|
1986
|
+
:param position: Information about where to place the entry in the experiment.
|
|
1987
|
+
:param is_field_addable: Whether users are able to add additional fields to this entry in the UI.
|
|
1988
|
+
:param is_existing_field_removable: Whether users are able to remove existing fields from this entry in the UI.
|
|
1989
|
+
:return: The newly created table entry.
|
|
1990
|
+
"""
|
|
1991
|
+
return self._create_eln_dt_table_step(entry_name, ElnBaseDataType.SAMPLE_DETAIL, fields, field_maps,
|
|
1992
|
+
position=position,
|
|
1993
|
+
is_field_addable=is_field_addable,
|
|
1994
|
+
is_existing_field_removable=is_existing_field_removable)
|
|
1995
|
+
|
|
1996
|
+
def _create_eln_dt_table_step(self, entry_name: str,
|
|
1997
|
+
dt: ElnBaseDataType,
|
|
1998
|
+
fields: list[ElnDataTypeFields] | None = None,
|
|
1999
|
+
field_maps: list[FieldMap] | None = None, *,
|
|
2000
|
+
position: ElnEntryPosition | None = None,
|
|
2001
|
+
is_field_addable: bool | None = None,
|
|
2002
|
+
is_existing_field_removable: bool | None = None) -> ElnEntryStep:
|
|
2003
|
+
fields: list[AbstractVeloxFieldDefinition] | None = self._to_field_defs(fields, dt)
|
|
2004
|
+
step = self._create_step(ElnEntryType.Table, entry_name, dt.data_type_name,
|
|
2005
|
+
position,
|
|
2006
|
+
field_definition_list=fields,
|
|
2007
|
+
field_map_list=field_maps)
|
|
2008
|
+
if is_field_addable is not None or is_existing_field_removable is not None:
|
|
2009
|
+
update = ElnTableEntryUpdateCriteria()
|
|
2010
|
+
update.is_field_addable = is_field_addable
|
|
2011
|
+
update.is_existing_field_removable = is_existing_field_removable
|
|
2012
|
+
self.force_step_update(step, update)
|
|
2013
|
+
return step
|
|
2014
|
+
|
|
2015
|
+
def create_temp_data_step(self, entry_name: str, data_type: DataTypeIdentifier, plugin_path: str, *,
|
|
2016
|
+
position: ElnEntryPosition | None = None) -> ElnEntryStep:
|
|
1699
2017
|
"""
|
|
1700
|
-
|
|
1701
|
-
entry: ExperimentEntry = step.eln_entry
|
|
1702
|
-
return ElnEntryPosition(entry.notebook_experiment_tab_id,
|
|
1703
|
-
entry.order,
|
|
1704
|
-
entry.column_span,
|
|
1705
|
-
entry.column_order)
|
|
2018
|
+
Create a new temp data entry in the experiment.
|
|
1706
2019
|
|
|
1707
|
-
|
|
2020
|
+
:param entry_name: The name of the entry.
|
|
2021
|
+
:param data_type: The data type of the entry.
|
|
2022
|
+
:param plugin_path: The plugin path to the plugin that populates the entry.
|
|
2023
|
+
:param position: Information about where to place the entry in the experiment.
|
|
2024
|
+
:return: The newly created temp data entry.
|
|
1708
2025
|
"""
|
|
1709
|
-
|
|
2026
|
+
return self._create_step(ElnEntryType.TempData, entry_name, data_type, position,
|
|
2027
|
+
temp_data_plugin_path=plugin_path)
|
|
1710
2028
|
|
|
1711
|
-
|
|
1712
|
-
|
|
2029
|
+
def create_text_step(self, entry_name: str, text: str | None = None, *,
|
|
2030
|
+
position: ElnEntryPosition | None = None) -> ElnEntryStep:
|
|
2031
|
+
"""
|
|
2032
|
+
Create a new text entry in the experiment.
|
|
1713
2033
|
|
|
1714
|
-
:param
|
|
1715
|
-
:
|
|
2034
|
+
:param entry_name: The name of the entry.
|
|
2035
|
+
:param text: The text to populate the entry with.
|
|
2036
|
+
:param position: Information about where to place the entry in the experiment.
|
|
2037
|
+
:return: The newly created text entry.
|
|
1716
2038
|
"""
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
2039
|
+
step: ElnEntryStep = self._create_step(ElnEntryType.Text, entry_name,
|
|
2040
|
+
ElnBaseDataType.TEXT_ENTRY_DETAIL.data_type_name, position)
|
|
2041
|
+
if text:
|
|
2042
|
+
text_record: DataRecord = self.get_step_records(entry_name)[0]
|
|
2043
|
+
text_record.set_field_value(ElnBaseDataType.get_text_entry_data_field_name(), text)
|
|
2044
|
+
DataRecordManager(self.user).commit_data_records([text_record])
|
|
2045
|
+
return step
|
|
2046
|
+
|
|
2047
|
+
def create_plate_designer_step(self, entry_name: str, source_entry: Step, *,
|
|
2048
|
+
position: ElnEntryPosition | None = None) -> ElnEntryStep:
|
|
2049
|
+
"""
|
|
2050
|
+
Create a new 3D plate designer entry in the experiment.
|
|
2051
|
+
|
|
2052
|
+
:param entry_name: The name of the entry.
|
|
2053
|
+
:param source_entry: The entry that the plate designer will source its samples from.
|
|
2054
|
+
:param position: Information about where to place the entry in the experiment.
|
|
2055
|
+
:return: The newly created plate designer entry.
|
|
2056
|
+
"""
|
|
2057
|
+
step = self.create_plugin_step(entry_name, "Sample", PLATE_DESIGNER_PLUGIN, position=position)
|
|
2058
|
+
default_layer = MultiLayerPlateLayer(
|
|
2059
|
+
MultiLayerDataTypeConfig("Sample"),
|
|
2060
|
+
PlatingOrder.FillBy.BY_COLUMN,
|
|
2061
|
+
MultiLayerReplicateConfig(),
|
|
2062
|
+
MultiLayerDilutionConfig()
|
|
2063
|
+
)
|
|
2064
|
+
initial_step_options: dict[str, str] = {
|
|
2065
|
+
"MultiLayerPlating_Plate_RecordIdList": "",
|
|
2066
|
+
"MultiLayerPlating_Entry_Prefs": MultiLayerPlatingManager.get_entry_prefs_json([default_layer]),
|
|
2067
|
+
"MultiLayerPlating_Entry_PrePlating_Prefs": MultiLayerPlatingManager.get_plate_configs_json(MultiLayerPlateConfig())
|
|
2068
|
+
}
|
|
2069
|
+
source_entry = self.__to_eln_step(source_entry)
|
|
2070
|
+
self.force_step_update_params(step, entry_height=600, entry_options_map=initial_step_options,
|
|
2071
|
+
related_entry_set=[source_entry.get_id()])
|
|
2072
|
+
return step
|
|
1729
2073
|
|
|
1730
|
-
#
|
|
1731
|
-
|
|
2074
|
+
# TODO: Update these functions to use entry type-specific creation criteria once Sapiopylib supports that.
|
|
2075
|
+
# This should take in an entry creation criteria object that handles all the abstract attributes that
|
|
2076
|
+
# every entry type shares.
|
|
2077
|
+
def _create_step(self, entry_type: ElnEntryType, entry_name: str, data_type: DataTypeIdentifier,
|
|
2078
|
+
position: ElnEntryPosition | None = None, **kwargs) \
|
|
2079
|
+
-> ElnEntryStep:
|
|
2080
|
+
"""
|
|
2081
|
+
Create a new entry in the experiment of the given type.
|
|
1732
2082
|
"""
|
|
1733
|
-
|
|
2083
|
+
if position is not None:
|
|
2084
|
+
order: int = position.order
|
|
2085
|
+
tab_id: int = position.tab_id
|
|
2086
|
+
column_order: int = position.column_order
|
|
2087
|
+
column_span: int = position.column_span
|
|
2088
|
+
else:
|
|
2089
|
+
last_tab: ElnExperimentTab = self.get_last_tab()
|
|
2090
|
+
order: int = self.get_next_entry_order_in_tab(last_tab)
|
|
2091
|
+
tab_id: int = last_tab.tab_id
|
|
2092
|
+
column_order: int = 0
|
|
2093
|
+
column_span: int = last_tab.max_number_of_columns
|
|
2094
|
+
|
|
2095
|
+
data_type: str = AliasUtil.to_data_type_name(data_type)
|
|
2096
|
+
crit = ElnEntryCriteria(entry_type, entry_name, data_type, order,
|
|
2097
|
+
notebook_experiment_tab_id=tab_id,
|
|
2098
|
+
column_order=column_order,
|
|
2099
|
+
column_span=column_span,
|
|
2100
|
+
**kwargs)
|
|
2101
|
+
entry: ExperimentEntry = self._eln_man.add_experiment_entry(self._exp_id, crit)
|
|
1734
2102
|
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
2103
|
+
self.add_entry_to_caches(entry)
|
|
2104
|
+
return ElnEntryStep(self._protocol, entry)
|
|
2105
|
+
|
|
2106
|
+
def _to_field_defs(self, fields: list[ElnDataTypeFields], dt: ElnBaseDataType) \
|
|
2107
|
+
-> list[AbstractVeloxFieldDefinition] | None:
|
|
2108
|
+
"""
|
|
2109
|
+
Convert a list of ElnDataTypeField aliases to field definitions.
|
|
1739
2110
|
"""
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2111
|
+
if not fields:
|
|
2112
|
+
return None
|
|
2113
|
+
field_defs: list[AbstractVeloxFieldDefinition] = []
|
|
2114
|
+
for field in fields:
|
|
2115
|
+
if isinstance(field, AbstractVeloxFieldDefinition):
|
|
2116
|
+
field_defs.append(field)
|
|
2117
|
+
elif isinstance(field, ElnFieldSetInfo):
|
|
2118
|
+
field_defs.extend(self._eln_man.get_predefined_fields_from_field_set_id(field.field_set_id))
|
|
2119
|
+
elif isinstance(field, str):
|
|
2120
|
+
field_defs.append(self._predefined_field(field, dt))
|
|
2121
|
+
elif isinstance(field, int):
|
|
2122
|
+
field_defs.extend(self._eln_man.get_predefined_fields_from_field_set_id(field))
|
|
2123
|
+
return field_defs
|
|
2124
|
+
|
|
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]
|
|
1743
2134
|
|
|
1744
2135
|
def __to_eln_step(self, step: Step) -> ElnEntryStep:
|
|
1745
2136
|
"""
|
|
@@ -1749,22 +2140,14 @@ class ExperimentHandler:
|
|
|
1749
2140
|
|
|
1750
2141
|
:return: The input step as an ElnEntryStep.
|
|
1751
2142
|
"""
|
|
1752
|
-
if isinstance(step, str)
|
|
1753
|
-
return self.get_step(step)
|
|
1754
|
-
if isinstance(step, int):
|
|
1755
|
-
return self._steps_by_id.get(step)
|
|
1756
|
-
if isinstance(step, ExperimentEntry):
|
|
1757
|
-
return self.add_entry_to_caches(step)
|
|
1758
|
-
return step
|
|
2143
|
+
return self.get_step(step) if isinstance(step, str) else step
|
|
1759
2144
|
|
|
1760
2145
|
def __to_eln_tab(self, tab: Tab) -> ElnExperimentTab:
|
|
1761
2146
|
"""
|
|
1762
|
-
Convert a variable that could be either a
|
|
2147
|
+
Convert a variable that could be either a string or an ElnExperimentTab to just an ElnExperimentTab.
|
|
1763
2148
|
This will query and cache the tabs for the experiment if the input tab is a name and the tabs have not been
|
|
1764
2149
|
cached before.
|
|
1765
2150
|
|
|
1766
2151
|
:return: The input tab as an ElnExperimentTab.
|
|
1767
2152
|
"""
|
|
1768
|
-
if
|
|
1769
|
-
return self.get_tab(tab)
|
|
1770
|
-
return tab
|
|
2153
|
+
return self.get_tab(tab) if isinstance(tab, str) else tab
|