sapiopycommons 2025.4.9a150__py3-none-any.whl → 2025.4.9a476__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 +1262 -392
- sapiopycommons/callbacks/field_builder.py +2 -0
- sapiopycommons/chem/Molecules.py +0 -2
- sapiopycommons/customreport/auto_pagers.py +281 -0
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/attachment_util.py +4 -2
- sapiopycommons/datatype/data_fields.py +23 -1
- sapiopycommons/eln/experiment_cache.py +173 -0
- sapiopycommons/eln/experiment_handler.py +933 -279
- sapiopycommons/eln/experiment_report_util.py +15 -10
- sapiopycommons/eln/experiment_step_factory.py +474 -0
- sapiopycommons/eln/experiment_tags.py +7 -0
- sapiopycommons/eln/plate_designer.py +159 -59
- sapiopycommons/eln/step_creation.py +235 -0
- sapiopycommons/files/file_bridge.py +76 -0
- sapiopycommons/files/file_bridge_handler.py +325 -110
- sapiopycommons/files/file_data_handler.py +2 -2
- sapiopycommons/files/file_util.py +40 -15
- sapiopycommons/files/file_validator.py +6 -5
- sapiopycommons/files/file_writer.py +1 -1
- sapiopycommons/flowcyto/flow_cyto.py +1 -1
- sapiopycommons/general/accession_service.py +3 -3
- sapiopycommons/general/aliases.py +51 -28
- sapiopycommons/general/audit_log.py +2 -2
- sapiopycommons/general/custom_report_util.py +24 -1
- sapiopycommons/general/data_structure_util.py +115 -0
- sapiopycommons/general/directive_util.py +86 -0
- sapiopycommons/general/exceptions.py +41 -2
- sapiopycommons/general/popup_util.py +2 -2
- sapiopycommons/multimodal/multimodal.py +1 -0
- sapiopycommons/processtracking/custom_workflow_handler.py +46 -30
- sapiopycommons/recordmodel/record_handler.py +547 -159
- sapiopycommons/rules/eln_rule_handler.py +41 -30
- sapiopycommons/rules/on_save_rule_handler.py +41 -30
- sapiopycommons/samples/aliquot.py +48 -0
- sapiopycommons/webhook/webhook_handlers.py +448 -55
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.4.9a150.dist-info → sapiopycommons-2025.4.9a476.dist-info}/METADATA +1 -1
- sapiopycommons-2025.4.9a476.dist-info/RECORD +67 -0
- sapiopycommons-2025.4.9a150.dist-info/RECORD +0 -59
- {sapiopycommons-2025.4.9a150.dist-info → sapiopycommons-2025.4.9a476.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.4.9a150.dist-info → sapiopycommons-2025.4.9a476.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import warnings
|
|
4
4
|
from collections.abc import Mapping, Iterable
|
|
5
|
+
from typing import TypeAlias
|
|
5
6
|
from weakref import WeakValueDictionary
|
|
6
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, TabIdentifier
|
|
12
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
13
|
+
from sapiopycommons.general.time_util import TimeUtil
|
|
14
|
+
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
7
15
|
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
8
16
|
from sapiopylib.rest.ELNService import ElnManager
|
|
9
17
|
from sapiopylib.rest.User import SapioUser
|
|
10
18
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
19
|
+
from sapiopylib.rest.pojo.eln.ElnEntryPosition import ElnEntryPosition
|
|
11
20
|
from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment, TemplateExperimentQueryPojo, ElnTemplate, \
|
|
12
21
|
InitializeNotebookExperimentPojo, ElnExperimentUpdateCriteria
|
|
13
|
-
from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry
|
|
14
|
-
|
|
22
|
+
from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry, ExperimentTableEntry, ExperimentFormEntry, \
|
|
23
|
+
ExperimentAttachmentEntry, ExperimentPluginEntry, ExperimentDashboardEntry, ExperimentTextEntry, \
|
|
24
|
+
ExperimentTempDataEntry
|
|
25
|
+
from sapiopylib.rest.pojo.eln.ExperimentEntryCriteria import AbstractElnEntryUpdateCriteria, \
|
|
26
|
+
ElnTableEntryUpdateCriteria, ElnFormEntryUpdateCriteria, ElnAttachmentEntryUpdateCriteria, \
|
|
27
|
+
ElnPluginEntryUpdateCriteria, ElnDashboardEntryUpdateCriteria, ElnTextEntryUpdateCriteria, \
|
|
28
|
+
ElnTempDataEntryUpdateCriteria
|
|
15
29
|
from sapiopylib.rest.pojo.eln.SapioELNEnums import ExperimentEntryStatus, ElnExperimentStatus, ElnEntryType, \
|
|
16
30
|
ElnBaseDataType
|
|
31
|
+
from sapiopylib.rest.pojo.eln.eln_headings import ElnExperimentTab, ElnExperimentTabAddCriteria
|
|
32
|
+
from sapiopylib.rest.pojo.eln.protocol_template import ProtocolTemplateInfo
|
|
17
33
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
18
34
|
from sapiopylib.rest.pojo.webhook.WebhookDirective import ElnExperimentDirective
|
|
19
35
|
from sapiopylib.rest.pojo.webhook.WebhookResult import SapioWebhookResult
|
|
@@ -23,12 +39,7 @@ from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelInst
|
|
|
23
39
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
24
40
|
from sapiopylib.rest.utils.recordmodel.properties import Child
|
|
25
41
|
|
|
26
|
-
|
|
27
|
-
from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIdentifier, UserIdentifier, \
|
|
28
|
-
DataTypeIdentifier, RecordModel
|
|
29
|
-
from sapiopycommons.general.exceptions import SapioException
|
|
30
|
-
|
|
31
|
-
Step = str | ElnEntryStep
|
|
42
|
+
Step: TypeAlias = str | ExperimentEntryIdentifier
|
|
32
43
|
"""An object representing an identifier to an ElnEntryStep. May be either the name of the step or the ElnEntryStep
|
|
33
44
|
itself."""
|
|
34
45
|
|
|
@@ -37,53 +48,74 @@ itself."""
|
|
|
37
48
|
class ExperimentHandler:
|
|
38
49
|
user: SapioUser
|
|
39
50
|
context: SapioWebhookContext | None
|
|
40
|
-
"""The context that this handler is working from."""
|
|
51
|
+
"""The context that this handler is working from, if any."""
|
|
41
52
|
|
|
53
|
+
# CR-47485: Made variables protected instead of private.
|
|
42
54
|
# Basic experiment info from the context.
|
|
43
|
-
|
|
55
|
+
_eln_exp: ElnExperiment
|
|
44
56
|
"""The ELN experiment from the context."""
|
|
45
|
-
|
|
57
|
+
_protocol: ElnExperimentProtocol
|
|
46
58
|
"""The ELN experiment as a protocol."""
|
|
47
|
-
|
|
59
|
+
_exp_id: int
|
|
48
60
|
"""The ID of this experiment's notebook. Used for making update webservice calls."""
|
|
49
61
|
|
|
50
62
|
# Managers.
|
|
51
|
-
|
|
63
|
+
_eln_man: ElnManager
|
|
52
64
|
"""The ELN manager. Used for updating the experiment and its steps."""
|
|
53
|
-
|
|
65
|
+
_exp_cache: ExperimentCacheManager
|
|
66
|
+
"""The experiment cache manager. Used for caching experiment-related information."""
|
|
67
|
+
_inst_man: RecordModelInstanceManager
|
|
54
68
|
"""The record model instance manager. Used for wrapping the data records of a step as record models."""
|
|
69
|
+
_rec_handler: RecordHandler
|
|
70
|
+
"""The record handler. Also used for wrapping the data records of a step as record models."""
|
|
55
71
|
|
|
56
72
|
# Only a fraction of the information about the current experiment exists in the context. Much information requires
|
|
57
73
|
# additional queries to obtain, but may also be repeatedly accessed. In such cases, cache the information after it
|
|
58
74
|
# has been requested so that the user doesn't need to worry about caching it themselves.
|
|
59
75
|
# CR-46341: Replace class variables with instance variables.
|
|
60
|
-
|
|
76
|
+
_exp_record: DataRecord | None
|
|
61
77
|
"""The data record for this experiment. Only cached when first accessed."""
|
|
62
|
-
|
|
78
|
+
_exp_template: ElnTemplate | None
|
|
63
79
|
"""The template for this experiment. Only cached when first accessed."""
|
|
64
|
-
|
|
80
|
+
_exp_options: dict[str, str]
|
|
65
81
|
"""Experiment options for this experiment. Only cached when first accessed."""
|
|
66
82
|
|
|
67
|
-
|
|
83
|
+
_queried_all_steps: bool
|
|
68
84
|
"""Whether this ExperimentHandler has queried the system for all steps in the experiment."""
|
|
69
|
-
|
|
70
|
-
"""
|
|
71
|
-
|
|
85
|
+
_steps: list[ElnEntryStep]
|
|
86
|
+
"""The sorted list of steps for this experiment. All steps are cached the first time any individual step is accessed."""
|
|
87
|
+
_steps_by_name: dict[str, ElnEntryStep]
|
|
88
|
+
"""Steps from this experiment by their name. All steps are cached the first time any individual step is accessed."""
|
|
89
|
+
_steps_by_id: dict[int, ElnEntryStep]
|
|
90
|
+
"""Steps from this experiment by their ID. All steps are cached the first time any individual step is accessed."""
|
|
91
|
+
_step_options: dict[int, dict[str, str]]
|
|
72
92
|
"""Entry options for each step in this experiment. All entry options are cached the first time any individual step's
|
|
73
93
|
options are queried. The cache is updated whenever the entry options for a step are changed by this handler."""
|
|
74
94
|
|
|
95
|
+
_step_updates: dict[int, AbstractElnEntryUpdateCriteria]
|
|
96
|
+
"""A dictionary of entry updates that have been made by this handler. Used to batch update entries."""
|
|
97
|
+
|
|
98
|
+
_queried_all_tabs: bool
|
|
99
|
+
"""Whether this ExperimentHandler has queried the system for all tabs in the experiment."""
|
|
100
|
+
_tabs: list[ElnExperimentTab]
|
|
101
|
+
"""The sorted tabs for this experiment. Only cached when first accessed."""
|
|
102
|
+
_tabs_by_id: dict[int, ElnExperimentTab]
|
|
103
|
+
"""The tabs for this experiment by their ID. Only cached when first accessed."""
|
|
104
|
+
_tabs_by_name: dict[str, ElnExperimentTab]
|
|
105
|
+
"""The tabs for this experiment by their name. Only cached when first accessed."""
|
|
106
|
+
|
|
75
107
|
# Constants
|
|
76
|
-
|
|
108
|
+
_ENTRY_COMPLETE_STATUSES = [ExperimentEntryStatus.Completed, ExperimentEntryStatus.CompletedApproved]
|
|
77
109
|
"""The set of statuses that an ELN entry could have and be considered completed/submitted."""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
110
|
+
_ENTRY_LOCKED_STATUSES = [ExperimentEntryStatus.Completed, ExperimentEntryStatus.CompletedApproved,
|
|
111
|
+
ExperimentEntryStatus.Disabled, ExperimentEntryStatus.LockedAwaitingApproval,
|
|
112
|
+
ExperimentEntryStatus.LockedRejected]
|
|
81
113
|
"""The set of statuses that an ELN entry could have and be considered locked."""
|
|
82
|
-
|
|
114
|
+
_EXPERIMENT_COMPLETE_STATUSES = [ElnExperimentStatus.Completed, ElnExperimentStatus.CompletedApproved]
|
|
83
115
|
"""The set of statuses that an ELN experiment could have and be considered completed."""
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
116
|
+
_EXPERIMENT_LOCKED_STATUSES = [ElnExperimentStatus.Completed, ElnExperimentStatus.CompletedApproved,
|
|
117
|
+
ElnExperimentStatus.LockedRejected, ElnExperimentStatus.LockedAwaitingApproval,
|
|
118
|
+
ElnExperimentStatus.Canceled]
|
|
87
119
|
"""The set of statuses that an ELN experiment could have and be considered locked."""
|
|
88
120
|
|
|
89
121
|
__instances: WeakValueDictionary[str, ExperimentHandler] = WeakValueDictionary()
|
|
@@ -123,27 +155,44 @@ class ExperimentHandler:
|
|
|
123
155
|
experiment = param_results[2]
|
|
124
156
|
|
|
125
157
|
# Get the basic information about this experiment that already exists in the context and is often used.
|
|
126
|
-
self.
|
|
127
|
-
self.
|
|
128
|
-
self.
|
|
158
|
+
self._eln_exp = experiment
|
|
159
|
+
self._protocol = ElnExperimentProtocol(experiment, self.user)
|
|
160
|
+
self._exp_id = self._protocol.get_id()
|
|
129
161
|
|
|
130
162
|
# Grab various managers that may be used.
|
|
131
|
-
self.
|
|
132
|
-
self.
|
|
163
|
+
self._eln_man = DataMgmtServer.get_eln_manager(self.user)
|
|
164
|
+
self._exp_cache = ExperimentCacheManager(self.user)
|
|
165
|
+
self._inst_man = RecordModelManager(self.user).instance_manager
|
|
166
|
+
self._rec_handler = RecordHandler(self.user)
|
|
133
167
|
|
|
134
168
|
# Create empty caches to fill when necessary.
|
|
135
|
-
self.
|
|
136
|
-
self.
|
|
169
|
+
self._queried_all_steps = False
|
|
170
|
+
self._steps_by_name = {}
|
|
171
|
+
self._steps_by_id = {}
|
|
172
|
+
self._steps = []
|
|
173
|
+
self._step_options = {}
|
|
174
|
+
self._step_updates = {}
|
|
175
|
+
|
|
176
|
+
self._tabs = []
|
|
177
|
+
self._tabs_by_id = {}
|
|
178
|
+
self._tabs_by_name = {}
|
|
179
|
+
|
|
180
|
+
self._queried_all_tabs = False
|
|
181
|
+
|
|
137
182
|
# CR-46330: Cache any experiment entry information that might already exist in the context.
|
|
138
|
-
self.__queried_all_steps = False
|
|
139
183
|
# We can only trust the entries in the context if the experiment that this handler is for is the same as the
|
|
140
184
|
# one from the context.
|
|
141
185
|
if self.context is not None and self.context.eln_experiment == experiment:
|
|
186
|
+
cache_steps: list[ElnEntryStep] = []
|
|
142
187
|
if self.context.experiment_entry is not None:
|
|
143
|
-
|
|
188
|
+
cache_steps.append(ElnEntryStep(self._protocol, self.context.experiment_entry))
|
|
144
189
|
if self.context.experiment_entry_list is not None:
|
|
145
190
|
for entry in self.context.experiment_entry_list:
|
|
146
|
-
|
|
191
|
+
cache_steps.append(ElnEntryStep(self._protocol, entry))
|
|
192
|
+
for step in cache_steps:
|
|
193
|
+
self._steps.append(step)
|
|
194
|
+
self._steps_by_name.update({step.get_name(): step})
|
|
195
|
+
self._steps_by_id.update({step.get_id(): step})
|
|
147
196
|
|
|
148
197
|
@staticmethod
|
|
149
198
|
def __parse_params(context: UserIdentifier, experiment: ExperimentIdentifier | SapioRecord | None = None) \
|
|
@@ -180,10 +229,99 @@ class ExperimentHandler:
|
|
|
180
229
|
if not experiment:
|
|
181
230
|
raise SapioException(f"No experiment with record ID {record_id} located in the system.")
|
|
182
231
|
if experiment is None:
|
|
183
|
-
raise SapioException("Cannot initialize ExperimentHandler. No ELN Experiment found in the provided
|
|
232
|
+
raise SapioException("Cannot initialize ExperimentHandler. No ELN Experiment found in the provided "
|
|
233
|
+
"parameters.")
|
|
184
234
|
|
|
185
235
|
return user, context, experiment
|
|
186
236
|
|
|
237
|
+
@property
|
|
238
|
+
def protocol(self) -> ElnExperimentProtocol:
|
|
239
|
+
"""
|
|
240
|
+
The ELN experiment that this handler is for as a protocol object.
|
|
241
|
+
"""
|
|
242
|
+
return self._protocol
|
|
243
|
+
|
|
244
|
+
# CR-47485: Add methods for clearing and updating the caches of this ExperimentHandler.
|
|
245
|
+
def clear_all_caches(self) -> None:
|
|
246
|
+
"""
|
|
247
|
+
Clear all caches that this ExperimentHandler uses.
|
|
248
|
+
"""
|
|
249
|
+
self.clear_step_caches()
|
|
250
|
+
self.clear_experiment_caches()
|
|
251
|
+
self.clear_tab_caches()
|
|
252
|
+
|
|
253
|
+
def clear_step_caches(self) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Clear the step caches that this ExperimentHandler uses.
|
|
256
|
+
"""
|
|
257
|
+
self._queried_all_steps = False
|
|
258
|
+
self._steps.clear()
|
|
259
|
+
self._steps_by_name.clear()
|
|
260
|
+
self._steps_by_id.clear()
|
|
261
|
+
self._step_options.clear()
|
|
262
|
+
self._step_updates.clear()
|
|
263
|
+
|
|
264
|
+
def clear_experiment_caches(self) -> None:
|
|
265
|
+
"""
|
|
266
|
+
Clear the experiment information caches that this ExperimentHandler uses.
|
|
267
|
+
"""
|
|
268
|
+
self._exp_record = None
|
|
269
|
+
self._exp_template = None
|
|
270
|
+
self._exp_options = {}
|
|
271
|
+
|
|
272
|
+
def clear_tab_caches(self) -> None:
|
|
273
|
+
"""
|
|
274
|
+
Clear the tab caches that this ExperimentHandler uses.
|
|
275
|
+
"""
|
|
276
|
+
self._queried_all_tabs = False
|
|
277
|
+
self._tabs.clear()
|
|
278
|
+
self._tabs_by_id.clear()
|
|
279
|
+
self._tabs_by_name.clear()
|
|
280
|
+
|
|
281
|
+
def add_entry_to_caches(self, entry: ExperimentEntry | ElnEntryStep) -> ElnEntryStep:
|
|
282
|
+
"""
|
|
283
|
+
Add the given entry to the cache of steps for this experiment. This is necessary in order for certain methods to
|
|
284
|
+
work. You should only need to do this if you have created a new entry in your code using a method outside
|
|
285
|
+
of this ExperimentHandler.
|
|
286
|
+
|
|
287
|
+
:param entry: The entry to add to the cache.
|
|
288
|
+
:return: The entry that was added to the cache as an ElnEntryStep.
|
|
289
|
+
"""
|
|
290
|
+
if isinstance(entry, ExperimentEntry):
|
|
291
|
+
entry = ElnEntryStep(self._protocol, entry)
|
|
292
|
+
self._steps.append(entry)
|
|
293
|
+
self._steps_by_name.update({entry.get_name(): entry})
|
|
294
|
+
self._steps_by_id.update({entry.get_id(): entry})
|
|
295
|
+
# Skipping the options cache. The get_step_options method will update the cache when necessary.
|
|
296
|
+
return entry
|
|
297
|
+
|
|
298
|
+
def add_entries_to_caches(self, entries: list[ExperimentEntry | ElnEntryStep]) -> list[ElnEntryStep]:
|
|
299
|
+
"""
|
|
300
|
+
Add the given entries to the cache of steps for this experiment. This is necessary in order for certain methods
|
|
301
|
+
to work. You should only need to do this if you have created a new entry in your code using a method outside
|
|
302
|
+
of this ExperimentHandler.
|
|
303
|
+
|
|
304
|
+
:param entries: The entries to add to the cache.
|
|
305
|
+
:return: The entries that were added to the cache as ElnEntrySteps.
|
|
306
|
+
"""
|
|
307
|
+
new_entries: list[ElnEntryStep] = []
|
|
308
|
+
for entry in entries:
|
|
309
|
+
new_entries.append(self.add_entry_to_caches(entry))
|
|
310
|
+
return new_entries
|
|
311
|
+
|
|
312
|
+
def add_tab_to_cache(self, tab: ElnExperimentTab) -> None:
|
|
313
|
+
"""
|
|
314
|
+
Add the given tab to the cache of tabs for this experiment. This is necessary in order for certain methods
|
|
315
|
+
to work properly. You should only need to do this if you have created a new tab in your code using a method
|
|
316
|
+
outside of this ExperimentHandler.
|
|
317
|
+
|
|
318
|
+
:param tab: The tab to add to the cache.
|
|
319
|
+
"""
|
|
320
|
+
self._tabs.append(tab)
|
|
321
|
+
self._tabs.sort(key=lambda t: t.tab_order)
|
|
322
|
+
self._tabs_by_id[tab.tab_id] = tab
|
|
323
|
+
self._tabs_by_name[tab.tab_name] = tab
|
|
324
|
+
|
|
187
325
|
# FR-46495: Split the creation of the experiment in launch_experiment into a create_experiment function.
|
|
188
326
|
@staticmethod
|
|
189
327
|
def create_experiment(context: SapioWebhookContext,
|
|
@@ -211,22 +349,10 @@ class ExperimentHandler:
|
|
|
211
349
|
:param active_templates_only: Whether only active templates should be queried for.
|
|
212
350
|
:return: The newly created experiment.
|
|
213
351
|
"""
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
for template in templates:
|
|
219
|
-
if template.template_name != template_name:
|
|
220
|
-
continue
|
|
221
|
-
if template_version is not None and template.template_version != template_version:
|
|
222
|
-
continue
|
|
223
|
-
launch_template = template
|
|
224
|
-
break
|
|
225
|
-
if launch_template is None:
|
|
226
|
-
raise SapioException(f"No template with the name \"{template_name}\"" +
|
|
227
|
-
("" if template_version is None else f" and the version {template_version}") +
|
|
228
|
-
f" found.")
|
|
229
|
-
|
|
352
|
+
launch_template: ElnTemplate = ExperimentCacheManager(context).get_experiment_template(template_name,
|
|
353
|
+
active_templates_only,
|
|
354
|
+
template_version,
|
|
355
|
+
first_match=True)
|
|
230
356
|
if experiment_name is None:
|
|
231
357
|
experiment_name: str = launch_template.display_name
|
|
232
358
|
if parent_record is not None:
|
|
@@ -239,7 +365,8 @@ class ExperimentHandler:
|
|
|
239
365
|
template_name: str,
|
|
240
366
|
experiment_name: str | None = None,
|
|
241
367
|
parent_record: SapioRecord | None = None, *,
|
|
242
|
-
template_version: int | None = None,
|
|
368
|
+
template_version: int | None = None,
|
|
369
|
+
active_templates_only: bool = True) -> SapioWebhookResult:
|
|
243
370
|
"""
|
|
244
371
|
Create a SapioWebhookResult that, when returned by a webhook handler, sends the user to a new experiment of the
|
|
245
372
|
input template name.
|
|
@@ -275,24 +402,24 @@ class ExperimentHandler:
|
|
|
275
402
|
when the experiment template doesn't exist.
|
|
276
403
|
:return: This experiment's template. None if it has no template.
|
|
277
404
|
"""
|
|
278
|
-
template_id: int | None = self.
|
|
405
|
+
template_id: int | None = self._eln_exp.template_id
|
|
279
406
|
if template_id is None:
|
|
280
|
-
self.
|
|
407
|
+
self._exp_template = None
|
|
281
408
|
if exception_on_none:
|
|
282
|
-
raise SapioException(f"Experiment with ID {self.
|
|
409
|
+
raise SapioException(f"Experiment with ID {self._exp_id} has no template ID.")
|
|
283
410
|
return None
|
|
284
411
|
|
|
285
|
-
if not hasattr(self, "
|
|
412
|
+
if not hasattr(self, "_exp_template"):
|
|
286
413
|
# PR-46504: Allow inactive and non-latest version templates to be queried.
|
|
287
414
|
query = TemplateExperimentQueryPojo(template_id_white_list=[template_id],
|
|
288
415
|
active_templates_only=False,
|
|
289
416
|
latest_version_only=False)
|
|
290
|
-
templates: list[ElnTemplate] = self.
|
|
417
|
+
templates: list[ElnTemplate] = self._eln_man.get_template_experiment_list(query)
|
|
291
418
|
# PR-46504: Set the exp_template to None if there are no results.
|
|
292
|
-
self.
|
|
293
|
-
if self.
|
|
294
|
-
raise SapioException(f"Experiment template not found for experiment with ID {self.
|
|
295
|
-
return self.
|
|
419
|
+
self._exp_template = templates[0] if templates else None
|
|
420
|
+
if self._exp_template is None and exception_on_none:
|
|
421
|
+
raise SapioException(f"Experiment template not found for experiment with ID {self._exp_id}.")
|
|
422
|
+
return self._exp_template
|
|
296
423
|
|
|
297
424
|
# CR-46104: Change get_template_name to behave like NotebookProtocolImpl.getTemplateName (i.e. first see if the
|
|
298
425
|
# experiment template exists, and if not, see if the experiment record exists, instead of only checking the
|
|
@@ -309,18 +436,18 @@ class ExperimentHandler:
|
|
|
309
436
|
when the template name doesn't exist.
|
|
310
437
|
:return: The template name of the current experiment. None if it has no template name.
|
|
311
438
|
"""
|
|
312
|
-
if not hasattr(self, "
|
|
439
|
+
if not hasattr(self, "_exp_template"):
|
|
313
440
|
self.get_experiment_template(False)
|
|
314
|
-
if self.
|
|
441
|
+
if self._exp_template is None and not hasattr(self, "_exp_record"):
|
|
315
442
|
self.get_experiment_record(False)
|
|
316
443
|
|
|
317
444
|
name: str | None = None
|
|
318
|
-
if self.
|
|
319
|
-
name = self.
|
|
320
|
-
elif self.
|
|
321
|
-
name = self.
|
|
445
|
+
if self._exp_template is not None:
|
|
446
|
+
name = self._exp_template.template_name
|
|
447
|
+
elif self._exp_record is not None:
|
|
448
|
+
name = self._exp_record.get_field_value("TemplateExperimentName")
|
|
322
449
|
if name is None and exception_on_none:
|
|
323
|
-
raise SapioException(f"Template name not found for experiment with ID {self.
|
|
450
|
+
raise SapioException(f"Template name not found for experiment with ID {self._exp_id}.")
|
|
324
451
|
return name
|
|
325
452
|
|
|
326
453
|
def get_experiment_record(self, exception_on_none: bool = True) -> DataRecord | None:
|
|
@@ -331,21 +458,23 @@ class ExperimentHandler:
|
|
|
331
458
|
when the experiment record doesn't exist.
|
|
332
459
|
:return: The data record for this experiment. None if it has no record.
|
|
333
460
|
"""
|
|
334
|
-
if not hasattr(self, "
|
|
335
|
-
self.
|
|
336
|
-
if self.
|
|
337
|
-
raise SapioException(f"Experiment record not found for experiment with ID {self.
|
|
338
|
-
return self.
|
|
461
|
+
if not hasattr(self, "_exp_record"):
|
|
462
|
+
self._exp_record = self._protocol.get_record()
|
|
463
|
+
if self._exp_record is None and exception_on_none:
|
|
464
|
+
raise SapioException(f"Experiment record not found for experiment with ID {self._exp_id}.")
|
|
465
|
+
return self._exp_record
|
|
339
466
|
|
|
340
|
-
|
|
467
|
+
# CR-47491: Support not providing a wrapper type to receive PyRecordModels instead of WrappedRecordModels.
|
|
468
|
+
def get_experiment_model(self, wrapper_type: type[WrappedType] | None = None) -> WrappedType | PyRecordModel:
|
|
341
469
|
"""
|
|
342
470
|
Query for the data record of this experiment and wrap it as a record model with the given wrapper.
|
|
343
471
|
The returned record is cached by the ExperimentHandler.
|
|
344
472
|
|
|
345
|
-
:param wrapper_type: The record model wrapper to use.
|
|
473
|
+
:param wrapper_type: The record model wrapper to use. If not provided, the record is returned as a
|
|
474
|
+
PyRecordModel instead of a WrappedRecordModel.
|
|
346
475
|
:return: The record model for this experiment.
|
|
347
476
|
"""
|
|
348
|
-
return self.
|
|
477
|
+
return self._rec_handler.wrap_model(self.get_experiment_record(), wrapper_type)
|
|
349
478
|
|
|
350
479
|
def update_experiment(self,
|
|
351
480
|
experiment_name: str | None = None,
|
|
@@ -367,14 +496,14 @@ class ExperimentHandler:
|
|
|
367
496
|
criteria.new_experiment_name = experiment_name
|
|
368
497
|
criteria.new_experiment_status = experiment_status
|
|
369
498
|
criteria.experiment_option_map = experiment_option_map
|
|
370
|
-
self.
|
|
499
|
+
self._eln_man.update_notebook_experiment(self._exp_id, criteria)
|
|
371
500
|
|
|
372
501
|
if experiment_name is not None:
|
|
373
|
-
self.
|
|
502
|
+
self._eln_exp.notebook_experiment_name = experiment_name
|
|
374
503
|
if experiment_status is not None:
|
|
375
|
-
self.
|
|
504
|
+
self._eln_exp.notebook_experiment_status = experiment_status
|
|
376
505
|
if experiment_option_map is not None:
|
|
377
|
-
self.
|
|
506
|
+
self._exp_options = experiment_option_map
|
|
378
507
|
|
|
379
508
|
def get_experiment_option(self, option: str) -> str:
|
|
380
509
|
"""
|
|
@@ -399,7 +528,10 @@ class ExperimentHandler:
|
|
|
399
528
|
|
|
400
529
|
:return: The map of options for this experiment.
|
|
401
530
|
"""
|
|
402
|
-
|
|
531
|
+
if hasattr(self, "_exp_options"):
|
|
532
|
+
return self._exp_options
|
|
533
|
+
self._exp_options = self._eln_man.get_notebook_experiment_options(self._exp_id)
|
|
534
|
+
return self._exp_options
|
|
403
535
|
|
|
404
536
|
def add_experiment_options(self, mapping: Mapping[str, str]) -> None:
|
|
405
537
|
"""
|
|
@@ -424,7 +556,7 @@ class ExperimentHandler:
|
|
|
424
556
|
|
|
425
557
|
:return: True if the experiment's status is Completed or CompletedApproved. False otherwise.
|
|
426
558
|
"""
|
|
427
|
-
return self.
|
|
559
|
+
return self._eln_exp.notebook_experiment_status in self._EXPERIMENT_COMPLETE_STATUSES
|
|
428
560
|
|
|
429
561
|
def experiment_is_canceled(self) -> bool:
|
|
430
562
|
"""
|
|
@@ -432,7 +564,7 @@ class ExperimentHandler:
|
|
|
432
564
|
|
|
433
565
|
:return: True if the experiment's status is Canceled. False otherwise.
|
|
434
566
|
"""
|
|
435
|
-
return self.
|
|
567
|
+
return self._eln_exp.notebook_experiment_status == ElnExperimentStatus.Canceled
|
|
436
568
|
|
|
437
569
|
def experiment_is_locked(self) -> bool:
|
|
438
570
|
"""
|
|
@@ -441,29 +573,34 @@ class ExperimentHandler:
|
|
|
441
573
|
:return: True if the experiment's status is Completed, CompletedApproved, Canceled, LockedAwaitingApproval,
|
|
442
574
|
or LockedRejected. False otherwise.
|
|
443
575
|
"""
|
|
444
|
-
return self.
|
|
576
|
+
return self._eln_exp.notebook_experiment_status in self._EXPERIMENT_LOCKED_STATUSES
|
|
445
577
|
|
|
446
578
|
def complete_experiment(self) -> None:
|
|
447
579
|
"""
|
|
448
580
|
Set the experiment's status to Completed. Makes a webservice call to update the experiment. Checks if the
|
|
449
581
|
experiment is already completed, and does nothing if so.
|
|
582
|
+
|
|
583
|
+
NOTE: This will cause the usual process tracking logic to run as if you'd clicked the "Complete Experiment"
|
|
584
|
+
toolbar button. This includes moving the in process samples forward to the next step in the process.
|
|
450
585
|
"""
|
|
451
586
|
if not self.experiment_is_complete():
|
|
452
|
-
self.
|
|
453
|
-
self.
|
|
587
|
+
self._protocol.complete_protocol()
|
|
588
|
+
self._eln_exp.notebook_experiment_status = ElnExperimentStatus.Completed
|
|
454
589
|
|
|
455
590
|
def cancel_experiment(self) -> None:
|
|
456
591
|
"""
|
|
457
592
|
Set the experiment's status to Canceled. Makes a webservice call to update the experiment. Checks if the
|
|
458
593
|
experiment is already canceled, and does nothing if so.
|
|
459
594
|
|
|
460
|
-
NOTE: This will
|
|
461
|
-
|
|
462
|
-
|
|
595
|
+
NOTE: This will cause the usual process tracking logic to run as if you'd clicked the "Cancel Experiment"
|
|
596
|
+
toolbar button. This includes moving the in process samples back into the process queue for the current step.
|
|
597
|
+
|
|
598
|
+
On version 24.12 and earlier, this was not the case, as the process tracking logic was tied to the button
|
|
599
|
+
instead of being on the experiment status change.
|
|
463
600
|
"""
|
|
464
601
|
if not self.experiment_is_canceled():
|
|
465
|
-
self.
|
|
466
|
-
self.
|
|
602
|
+
self._protocol.cancel_protocol()
|
|
603
|
+
self._eln_exp.notebook_experiment_status = ElnExperimentStatus.Canceled
|
|
467
604
|
|
|
468
605
|
def step_exists(self, step_name: str) -> bool:
|
|
469
606
|
"""
|
|
@@ -489,14 +626,14 @@ class ExperimentHandler:
|
|
|
489
626
|
"""
|
|
490
627
|
return all([x is not None for x in self.get_steps(step_names, False)])
|
|
491
628
|
|
|
492
|
-
def get_step(self, step_name: str, exception_on_none: bool = True) -> ElnEntryStep | None:
|
|
629
|
+
def get_step(self, step_name: str | int, exception_on_none: bool = True) -> ElnEntryStep | None:
|
|
493
630
|
"""
|
|
494
631
|
Get the step of the given name from the experiment.
|
|
495
632
|
|
|
496
633
|
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
497
634
|
list of steps in the experiment and caches them.
|
|
498
635
|
|
|
499
|
-
:param step_name: The name for the step to return.
|
|
636
|
+
:param step_name: The name or ID for the step to return.
|
|
500
637
|
:param exception_on_none: If false, returns None if the entry can't be found. If true, raises an exception
|
|
501
638
|
when the named entry doesn't exist in the experiment.
|
|
502
639
|
:return: An ElnEntrySteps matching the provided name. If there is no match and no exception is to be thrown,
|
|
@@ -504,31 +641,32 @@ class ExperimentHandler:
|
|
|
504
641
|
"""
|
|
505
642
|
return self.get_steps([step_name], exception_on_none)[0]
|
|
506
643
|
|
|
507
|
-
def get_steps(self, step_names: Iterable[str], exception_on_none: bool = True) -> list[ElnEntryStep | None]:
|
|
644
|
+
def get_steps(self, step_names: Iterable[str | int], exception_on_none: bool = True) -> list[ElnEntryStep | None]:
|
|
508
645
|
"""
|
|
509
646
|
Get a list of steps of the given names from the experiment, sorted in the same order as the names are provided.
|
|
510
647
|
|
|
511
648
|
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
512
649
|
list of steps in the experiment and caches them.
|
|
513
650
|
|
|
514
|
-
:param step_names: A list of names for the entries to return and the order to return them in.
|
|
651
|
+
:param step_names: A list of names or IDs for the entries to return and the order to return them in.
|
|
515
652
|
:param exception_on_none: If false, returns None for entries that can't be found. If true, raises an exception
|
|
516
653
|
when the named entry doesn't exist in the experiment.
|
|
517
654
|
:return: A list of ElnEntrySteps matching the provided names in the order they were provided in. If there is no
|
|
518
655
|
match for a given step and no exception is to be thrown, returns None for that step.
|
|
519
656
|
"""
|
|
520
657
|
ret_list: list[ElnEntryStep | None] = []
|
|
521
|
-
for
|
|
658
|
+
for step_id in step_names:
|
|
522
659
|
# If we haven't queried the system for all steps in the experiment yet, then the reason that a step is
|
|
523
660
|
# missing may be because it wasn't in the webhook context. Therefore, query all steps and then check
|
|
524
661
|
# if the step name is still missing from the experiment before potentially throwing an exception.
|
|
525
|
-
if self.
|
|
526
|
-
self.
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
662
|
+
if self._queried_all_steps is False and step_id not in self._steps_by_name and step_id not in self._steps_by_id:
|
|
663
|
+
self._query_all_steps()
|
|
664
|
+
if isinstance(step_id, str):
|
|
665
|
+
step: ElnEntryStep = self._steps_by_name.get(step_id)
|
|
666
|
+
else:
|
|
667
|
+
step: ElnEntryStep = self._steps_by_id.get(step_id)
|
|
530
668
|
if step is None and exception_on_none is True:
|
|
531
|
-
raise SapioException(f"ElnEntryStep of name \"{
|
|
669
|
+
raise SapioException(f"ElnEntryStep of name \"{step_id}\" not found in experiment with ID {self._exp_id}.")
|
|
532
670
|
ret_list.append(step)
|
|
533
671
|
return ret_list
|
|
534
672
|
|
|
@@ -542,15 +680,35 @@ class ExperimentHandler:
|
|
|
542
680
|
a data type name or wrapper is given, only returns entries that match that data type name or wrapper.
|
|
543
681
|
:return: Every entry in the experiment in order of appearance that match the provided data type, if any.
|
|
544
682
|
"""
|
|
545
|
-
if self.
|
|
546
|
-
self.
|
|
547
|
-
|
|
548
|
-
|
|
683
|
+
if self._queried_all_steps is False:
|
|
684
|
+
self._query_all_steps()
|
|
685
|
+
else:
|
|
686
|
+
# Re-sort the steps in case any new steps were added before the last time that this was called.
|
|
687
|
+
def sort_steps(step: ElnEntryStep) -> tuple:
|
|
688
|
+
entry = step.eln_entry
|
|
689
|
+
tab_order: int = self.get_tab(entry.notebook_experiment_tab_id).tab_order
|
|
690
|
+
entry_order: int = entry.order
|
|
691
|
+
column_order: int = entry.column_order
|
|
692
|
+
return tab_order, entry_order, column_order
|
|
693
|
+
|
|
694
|
+
self._steps.sort(key=sort_steps)
|
|
695
|
+
all_steps: list[ElnEntryStep] = self._steps
|
|
549
696
|
if data_type is None:
|
|
550
697
|
return all_steps
|
|
551
698
|
data_type: str = AliasUtil.to_data_type_name(data_type)
|
|
552
699
|
return [x for x in all_steps if data_type in x.get_data_type_names()]
|
|
553
700
|
|
|
701
|
+
def _query_all_steps(self) -> None:
|
|
702
|
+
"""
|
|
703
|
+
Query the system for every step in the experiment and cache them.
|
|
704
|
+
"""
|
|
705
|
+
self._queried_all_steps = True
|
|
706
|
+
self._protocol.invalidate()
|
|
707
|
+
self._steps = self._protocol.get_sorted_step_list()
|
|
708
|
+
for step in self._steps:
|
|
709
|
+
self._steps_by_name[step.get_name()] = step
|
|
710
|
+
self._steps_by_id[step.get_id()] = step
|
|
711
|
+
|
|
554
712
|
def get_step_by_option(self, key: str, value: str | None = None) -> ElnEntryStep:
|
|
555
713
|
"""
|
|
556
714
|
Retrieve the step in this experiment that contains an entry option with the provided key and value.
|
|
@@ -599,7 +757,8 @@ class ExperimentHandler:
|
|
|
599
757
|
"""
|
|
600
758
|
return self.__to_eln_step(step).get_records()
|
|
601
759
|
|
|
602
|
-
def get_step_models(self, step: Step, wrapper_type: type[WrappedType])
|
|
760
|
+
def get_step_models(self, step: Step, wrapper_type: type[WrappedType] | None = None) \
|
|
761
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
603
762
|
"""
|
|
604
763
|
Query for the data records for the given step and wrap them as record models with the given type. The returned
|
|
605
764
|
records are not cached by the ExperimentHandler.
|
|
@@ -611,10 +770,11 @@ class ExperimentHandler:
|
|
|
611
770
|
The step to get the data records for.
|
|
612
771
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
613
772
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
614
|
-
:param wrapper_type: The record model wrapper to use.
|
|
773
|
+
:param wrapper_type: The record model wrapper to use. If not provided, the records are returned as
|
|
774
|
+
PyRecordModels instead of WrappedRecordModels.
|
|
615
775
|
:return: The record models for the given step.
|
|
616
776
|
"""
|
|
617
|
-
return self.
|
|
777
|
+
return self._rec_handler.wrap_models(self.get_step_records(step), wrapper_type)
|
|
618
778
|
|
|
619
779
|
def add_step_records(self, step: Step, records: Iterable[SapioRecord]) -> None:
|
|
620
780
|
"""
|
|
@@ -637,8 +797,9 @@ class ExperimentHandler:
|
|
|
637
797
|
return
|
|
638
798
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
639
799
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
640
|
-
raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. "
|
|
641
|
-
f"
|
|
800
|
+
raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. ELN "
|
|
801
|
+
f"records that are committed to the system will automatically appear in the ELN entry "
|
|
802
|
+
f"with the matching data type name.")
|
|
642
803
|
if dt != step.get_data_type_names()[0]:
|
|
643
804
|
raise SapioException(f"Cannot add {dt} records to entry {step.get_name()} of type "
|
|
644
805
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -664,8 +825,9 @@ class ExperimentHandler:
|
|
|
664
825
|
return
|
|
665
826
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
666
827
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
667
|
-
|
|
668
|
-
|
|
828
|
+
# CR-47532: Add remove_step_records support for Experiment Detail and Sample Detail entries.
|
|
829
|
+
self.remove_eln_rows(step, records)
|
|
830
|
+
return
|
|
669
831
|
if dt != step.get_data_type_names()[0]:
|
|
670
832
|
raise SapioException(f"Cannot remove {dt} records from entry {step.get_name()} of type "
|
|
671
833
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -694,9 +856,14 @@ class ExperimentHandler:
|
|
|
694
856
|
step = self.__to_eln_step(step)
|
|
695
857
|
if records:
|
|
696
858
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
859
|
+
# CR-47532: Add set_step_records support for Experiment Detail and Sample Detail entries.
|
|
697
860
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
698
|
-
|
|
699
|
-
|
|
861
|
+
remove_rows: list[PyRecordModel] = []
|
|
862
|
+
for record in self.get_step_models(step):
|
|
863
|
+
if record not in records:
|
|
864
|
+
remove_rows.append(record)
|
|
865
|
+
self.remove_eln_rows(step, remove_rows)
|
|
866
|
+
return
|
|
700
867
|
if dt != step.get_data_type_names()[0]:
|
|
701
868
|
raise SapioException(f"Cannot set {dt} records for entry {step.get_name()} of type "
|
|
702
869
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -718,10 +885,30 @@ class ExperimentHandler:
|
|
|
718
885
|
The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
|
|
719
886
|
"""
|
|
720
887
|
self.set_step_records(step, [record])
|
|
888
|
+
step = self.__to_eln_step(step)
|
|
889
|
+
if isinstance(step.eln_entry, ExperimentFormEntry):
|
|
890
|
+
step.eln_entry.record_id = AliasUtil.to_data_record(record).record_id
|
|
721
891
|
|
|
722
892
|
# FR-46496 - Provide functions for adding and removing rows from an ELN data type entry.
|
|
893
|
+
def add_eln_row(self, step: Step, wrapper_type: type[WrappedType] | None = None) -> WrappedType | PyRecordModel:
|
|
894
|
+
"""
|
|
895
|
+
Add a row to an ELNExperimentDetail or ELNSampleDetail table entry. The row will not appear in the system
|
|
896
|
+
until a record manager store and commit has occurred.
|
|
897
|
+
|
|
898
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
899
|
+
list of steps in the experiment and caches them.
|
|
900
|
+
|
|
901
|
+
:param step:
|
|
902
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
903
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
904
|
+
:param wrapper_type: Optionally wrap the ELN data type in a record model wrapper. If not provided, returns
|
|
905
|
+
an unwrapped PyRecordModel.
|
|
906
|
+
:return: The newly created row.
|
|
907
|
+
"""
|
|
908
|
+
return self.add_eln_rows(step, 1, wrapper_type)[0]
|
|
909
|
+
|
|
723
910
|
def add_eln_rows(self, step: Step, count: int, wrapper_type: type[WrappedType] | None = None) \
|
|
724
|
-
-> list[
|
|
911
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
725
912
|
"""
|
|
726
913
|
Add rows to an ELNExperimentDetail or ELNSampleDetail table entry. The rows will not appear in the system
|
|
727
914
|
until a record manager store and commit has occurred.
|
|
@@ -743,15 +930,69 @@ class ExperimentHandler:
|
|
|
743
930
|
dt: str = step.get_data_type_names()[0]
|
|
744
931
|
if not ElnBaseDataType.is_eln_type(dt):
|
|
745
932
|
raise SapioException("The provided step is not an ELN data type entry.")
|
|
746
|
-
records: list[PyRecordModel] = self.
|
|
933
|
+
records: list[PyRecordModel] = self._inst_man.add_new_records(dt, count)
|
|
747
934
|
if wrapper_type:
|
|
748
|
-
return self.
|
|
935
|
+
return self._inst_man.wrap_list(records, wrapper_type)
|
|
749
936
|
return records
|
|
750
937
|
|
|
751
|
-
def
|
|
938
|
+
def add_sample_detail(self, step: Step, sample: RecordModel,
|
|
939
|
+
wrapper_type: type[WrappedType] | None = None) \
|
|
940
|
+
-> WrappedType | PyRecordModel:
|
|
752
941
|
"""
|
|
753
|
-
Add a
|
|
754
|
-
|
|
942
|
+
Add a sample detail to a sample detail entry while relating it to the input sample record.
|
|
943
|
+
|
|
944
|
+
:param step:
|
|
945
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
946
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
947
|
+
:param sample: The sample record to add the sample detail to.
|
|
948
|
+
:param wrapper_type: Optionally wrap the sample detail in a record model wrapper. If not provided, returns
|
|
949
|
+
an unwrapped PyRecordModel.
|
|
950
|
+
:return: The newly created sample detail.
|
|
951
|
+
"""
|
|
952
|
+
return self.add_sample_details(step, [sample], wrapper_type)[0]
|
|
953
|
+
|
|
954
|
+
def add_sample_details(self, step: Step, samples: Iterable[RecordModel],
|
|
955
|
+
wrapper_type: type[WrappedType] | None = None) \
|
|
956
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
957
|
+
"""
|
|
958
|
+
Add sample details to a sample details entry while relating them to the input sample records.
|
|
959
|
+
|
|
960
|
+
:param step:
|
|
961
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
962
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
963
|
+
:param samples: The sample records to add the sample details to.
|
|
964
|
+
:param wrapper_type: Optionally wrap the sample details in a record model wrapper. If not provided, returns
|
|
965
|
+
an unwrapped PyRecordModel.
|
|
966
|
+
:return: The newly created sample details. The indices of the samples in the input list match the index of the
|
|
967
|
+
sample details in this list that they are related to.
|
|
968
|
+
"""
|
|
969
|
+
step = self.__to_eln_step(step)
|
|
970
|
+
if step.eln_entry.entry_type != ElnEntryType.Table:
|
|
971
|
+
raise SapioException("The provided step is not a table entry.")
|
|
972
|
+
dt: str = step.get_data_type_names()[0]
|
|
973
|
+
if not ElnBaseDataType.is_eln_type(dt) or ElnBaseDataType.get_base_type(dt) != ElnBaseDataType.SAMPLE_DETAIL:
|
|
974
|
+
raise SapioException("The provided step is not an ELNSampleDetail entry.")
|
|
975
|
+
records: list[PyRecordModel] = []
|
|
976
|
+
for sample in samples:
|
|
977
|
+
if sample.data_type_name != "Sample":
|
|
978
|
+
raise SapioException(f"Received a {sample.data_type_name} record when Sample records were expected.")
|
|
979
|
+
detail: PyRecordModel = sample.add(Child.create_by_name(dt))
|
|
980
|
+
detail.set_field_values({
|
|
981
|
+
"SampleId": sample.get_field_value("SampleId"),
|
|
982
|
+
"OtherSampleId": sample.get_field_value("OtherSampleId")
|
|
983
|
+
})
|
|
984
|
+
records.append(detail)
|
|
985
|
+
if wrapper_type:
|
|
986
|
+
return self._inst_man.wrap_list(records, wrapper_type)
|
|
987
|
+
return records
|
|
988
|
+
|
|
989
|
+
def remove_eln_row(self, step: Step, record: SapioRecord) -> None:
|
|
990
|
+
"""
|
|
991
|
+
Remove a row from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
|
|
992
|
+
records in the system that match the entry's data type. This means that removing rows from an ELN data type
|
|
993
|
+
table entry is equivalent to deleting the records for the rows.
|
|
994
|
+
|
|
995
|
+
The row will not be deleted in the system until a record manager store and commit has occurred.
|
|
755
996
|
|
|
756
997
|
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
757
998
|
list of steps in the experiment and caches them.
|
|
@@ -759,13 +1000,13 @@ class ExperimentHandler:
|
|
|
759
1000
|
:param step:
|
|
760
1001
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
761
1002
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
762
|
-
:param
|
|
763
|
-
|
|
764
|
-
|
|
1003
|
+
:param record:
|
|
1004
|
+
The record to remove from the given step.
|
|
1005
|
+
The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
|
|
765
1006
|
"""
|
|
766
|
-
|
|
1007
|
+
self.remove_eln_rows(step, [record])
|
|
767
1008
|
|
|
768
|
-
def remove_eln_rows(self, step: Step, records:
|
|
1009
|
+
def remove_eln_rows(self, step: Step, records: Iterable[SapioRecord]) -> None:
|
|
769
1010
|
"""
|
|
770
1011
|
Remove rows from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
|
|
771
1012
|
records in the system that match the entry's data type. This means that removing rows from an ELN data type
|
|
@@ -802,64 +1043,11 @@ class ExperimentHandler:
|
|
|
802
1043
|
else:
|
|
803
1044
|
record.delete()
|
|
804
1045
|
if data_records:
|
|
805
|
-
record_models: list[PyRecordModel] = self.
|
|
1046
|
+
record_models: list[PyRecordModel] = self._inst_man.add_existing_records(data_records)
|
|
806
1047
|
for record in record_models:
|
|
807
1048
|
record.delete()
|
|
808
1049
|
|
|
809
|
-
|
|
810
|
-
"""
|
|
811
|
-
Remove a row from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
|
|
812
|
-
records in the system that match the entry's data type. This means that removing rows from an ELN data type
|
|
813
|
-
table entry is equivalent to deleting the records for the rows.
|
|
814
|
-
|
|
815
|
-
The row will not be deleted in the system until a record manager store and commit has occurred.
|
|
816
|
-
|
|
817
|
-
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
818
|
-
list of steps in the experiment and caches them.
|
|
819
|
-
|
|
820
|
-
:param step:
|
|
821
|
-
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
822
|
-
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
823
|
-
:param record:
|
|
824
|
-
The record to remove from the given step.
|
|
825
|
-
The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
|
|
826
|
-
"""
|
|
827
|
-
self.remove_eln_rows(step, [record])
|
|
828
|
-
|
|
829
|
-
def add_sample_details(self, step: Step, samples: list[RecordModel], wrapper_type: type[WrappedType]) \
|
|
830
|
-
-> list[PyRecordModel | WrappedType]:
|
|
831
|
-
"""
|
|
832
|
-
Add sample details to a sample details entry while relating them to the input sample records.
|
|
833
|
-
|
|
834
|
-
:param step:
|
|
835
|
-
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
836
|
-
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
837
|
-
:param samples: The sample records to add the sample details to.
|
|
838
|
-
:param wrapper_type: Optionally wrap the sample details in a record model wrapper. If not provided, returns
|
|
839
|
-
an unwrapped PyRecordModel.
|
|
840
|
-
:return: The newly created sample details. The indices of the samples in the input list match the index of the
|
|
841
|
-
sample details in this list that they are related to.
|
|
842
|
-
"""
|
|
843
|
-
step = self.__to_eln_step(step)
|
|
844
|
-
if step.eln_entry.entry_type != ElnEntryType.Table:
|
|
845
|
-
raise SapioException("The provided step is not a table entry.")
|
|
846
|
-
dt: str = step.get_data_type_names()[0]
|
|
847
|
-
if not ElnBaseDataType.is_eln_type(dt) or ElnBaseDataType.get_base_type(dt) != ElnBaseDataType.SAMPLE_DETAIL:
|
|
848
|
-
raise SapioException("The provided step is not an ELNSampleDetail entry.")
|
|
849
|
-
records: list[PyRecordModel] = []
|
|
850
|
-
for sample in samples:
|
|
851
|
-
if sample.data_type_name != "Sample":
|
|
852
|
-
raise SapioException(f"Received a {sample.data_type_name} record when Sample records were expected.")
|
|
853
|
-
detail: PyRecordModel = sample.add(Child.create_by_name(dt))
|
|
854
|
-
detail.set_field_values({
|
|
855
|
-
"SampleId": sample.get_field_value("SampleId"),
|
|
856
|
-
"OtherSampleId": sample.get_field_value("OtherSampleId")
|
|
857
|
-
})
|
|
858
|
-
records.append(detail)
|
|
859
|
-
if wrapper_type:
|
|
860
|
-
return self.__inst_man.wrap_list(records, wrapper_type)
|
|
861
|
-
return records
|
|
862
|
-
|
|
1050
|
+
# noinspection PyPep8Naming
|
|
863
1051
|
def update_step(self, step: Step,
|
|
864
1052
|
entry_name: str | None = None,
|
|
865
1053
|
related_entry_set: Iterable[int] | None = None,
|
|
@@ -926,8 +1114,227 @@ class ExperimentHandler:
|
|
|
926
1114
|
If you wish to add options to the existing map of options that an entry has, use the
|
|
927
1115
|
add_step_options method.
|
|
928
1116
|
"""
|
|
1117
|
+
# FR-47468: Deprecating this since the parameters are ordered. The new method requires keyword parameters, so
|
|
1118
|
+
# that we can add new parameters wherever we want without breaking existing code.
|
|
1119
|
+
warnings.warn("Update step is deprecated. Use force_entry_update_params instead.",
|
|
1120
|
+
DeprecationWarning)
|
|
1121
|
+
self.force_step_update_params(step,
|
|
1122
|
+
entry_name=entry_name,
|
|
1123
|
+
related_entry_set=related_entry_set,
|
|
1124
|
+
dependency_set=dependency_set,
|
|
1125
|
+
entry_status=entry_status,
|
|
1126
|
+
order=order,
|
|
1127
|
+
description=description,
|
|
1128
|
+
requires_grabber_plugin=requires_grabber_plugin,
|
|
1129
|
+
is_initialization_required=is_initialization_required,
|
|
1130
|
+
notebook_experiment_tab_id=notebook_experiment_tab_id,
|
|
1131
|
+
entry_height=entry_height,
|
|
1132
|
+
column_order=column_order,
|
|
1133
|
+
column_span=column_span,
|
|
1134
|
+
is_removable=is_removable,
|
|
1135
|
+
is_renamable=is_renamable,
|
|
1136
|
+
source_entry_id=source_entry_id,
|
|
1137
|
+
clear_source_entry_id=clear_source_entry_id,
|
|
1138
|
+
is_hidden=is_hidden,
|
|
1139
|
+
is_static_view=is_static_View,
|
|
1140
|
+
is_shown_in_template=is_shown_in_template,
|
|
1141
|
+
template_item_fulfilled_timestamp=template_item_fulfilled_timestamp,
|
|
1142
|
+
clear_template_item_fulfilled_timestamp=clear_template_item_fulfilled_timestamp,
|
|
1143
|
+
entry_options_map=entry_options_map)
|
|
1144
|
+
|
|
1145
|
+
# FR-47468: Some functions that can help with entry updates.
|
|
1146
|
+
def force_step_update_params(self, step: Step, *,
|
|
1147
|
+
entry_name: str | None = None,
|
|
1148
|
+
related_entry_set: Iterable[int] | None = None,
|
|
1149
|
+
dependency_set: Iterable[int] | None = None,
|
|
1150
|
+
entry_status: ExperimentEntryStatus | None = None,
|
|
1151
|
+
order: int | None = None,
|
|
1152
|
+
description: str | None = None,
|
|
1153
|
+
requires_grabber_plugin: bool | None = None,
|
|
1154
|
+
is_initialization_required: bool | None = None,
|
|
1155
|
+
notebook_experiment_tab_id: int | None = None,
|
|
1156
|
+
entry_height: int | None = None,
|
|
1157
|
+
column_order: int | None = None,
|
|
1158
|
+
column_span: int | None = None,
|
|
1159
|
+
is_removable: bool | None = None,
|
|
1160
|
+
is_renamable: bool | None = None,
|
|
1161
|
+
source_entry_id: int | None = None,
|
|
1162
|
+
clear_source_entry_id: bool | None = None,
|
|
1163
|
+
is_hidden: bool | None = None,
|
|
1164
|
+
is_static_view: bool | None = None,
|
|
1165
|
+
is_shown_in_template: bool | None = None,
|
|
1166
|
+
template_item_fulfilled_timestamp: int | None = None,
|
|
1167
|
+
clear_template_item_fulfilled_timestamp: bool | None = None,
|
|
1168
|
+
entry_options_map: dict[str, str] | None = None) -> None:
|
|
1169
|
+
"""
|
|
1170
|
+
Immediately sent an update to an entry in this experiment. All changes will be reflected by the ExperimentEntry
|
|
1171
|
+
of the Step that is being updated.
|
|
1172
|
+
|
|
1173
|
+
Consider using store_step_update and commit_step_updates instead if the update does not need to be immediate.
|
|
1174
|
+
|
|
1175
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1176
|
+
list of steps in the experiment and caches them.
|
|
1177
|
+
|
|
1178
|
+
:param step:
|
|
1179
|
+
The entry step to update.
|
|
1180
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1181
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1182
|
+
:param entry_name: The new name of this entry.
|
|
1183
|
+
:param related_entry_set: The new set of entry IDs for the entries that are related (implicitly dependent) to
|
|
1184
|
+
this entry. Completely overwrites the existing related entries.
|
|
1185
|
+
:param dependency_set: The new set of entry IDs for the entries that are dependent (explicitly dependent) on
|
|
1186
|
+
this entry. Completely overwrites the existing dependent entries.
|
|
1187
|
+
:param entry_status: The new status of this entry.
|
|
1188
|
+
:param order: The row order of this entry in its tab.
|
|
1189
|
+
:param description: The new description of this entry.
|
|
1190
|
+
:param requires_grabber_plugin: Whether this entry's initialization is handled by a grabber plugin. If true,
|
|
1191
|
+
then is_initialization_required is forced to true by the server.
|
|
1192
|
+
:param is_initialization_required: Whether the user is required to manually initialize this entry.
|
|
1193
|
+
:param notebook_experiment_tab_id: The ID of the tab that this entry should appear on.
|
|
1194
|
+
:param entry_height: The height of this entry.
|
|
1195
|
+
:param column_order: The column order of this entry.
|
|
1196
|
+
:param column_span: How many columns this entry spans.
|
|
1197
|
+
:param is_removable: Whether this entry can be removed by the user.
|
|
1198
|
+
:param is_renamable: Whether this entry can be renamed by the user.
|
|
1199
|
+
:param source_entry_id: The ID of this entry from its template.
|
|
1200
|
+
:param clear_source_entry_id: True if the source entry ID should be cleared.
|
|
1201
|
+
:param is_hidden: Whether this entry is hidden from the user.
|
|
1202
|
+
:param is_static_view: Whether this entry is static. Static entries are uneditable and shared across all
|
|
1203
|
+
experiments of the same template.
|
|
1204
|
+
:param is_shown_in_template: Whether this entry is saved to and shown in the experiment's template.
|
|
1205
|
+
:param template_item_fulfilled_timestamp: A timestamp in milliseconds for when this entry was initialized.
|
|
1206
|
+
:param clear_template_item_fulfilled_timestamp: True if the template item fulfilled timestamp should be cleared,
|
|
1207
|
+
uninitializing the entry.
|
|
1208
|
+
:param entry_options_map:
|
|
1209
|
+
The new map of options for this entry. Completely overwrites the existing options map.
|
|
1210
|
+
Any changes to the entry options will update this ExperimentHandler's cache of entry options.
|
|
1211
|
+
If you wish to add options to the existing map of options that an entry has, use the
|
|
1212
|
+
add_step_options method.
|
|
1213
|
+
"""
|
|
1214
|
+
update = self._criteria_from_params(step,
|
|
1215
|
+
entry_name=entry_name,
|
|
1216
|
+
related_entry_set=related_entry_set,
|
|
1217
|
+
dependency_set=dependency_set,
|
|
1218
|
+
entry_status=entry_status,
|
|
1219
|
+
order=order,
|
|
1220
|
+
description=description,
|
|
1221
|
+
requires_grabber_plugin=requires_grabber_plugin,
|
|
1222
|
+
is_initialization_required=is_initialization_required,
|
|
1223
|
+
notebook_experiment_tab_id=notebook_experiment_tab_id,
|
|
1224
|
+
entry_height=entry_height,
|
|
1225
|
+
column_order=column_order,
|
|
1226
|
+
column_span=column_span,
|
|
1227
|
+
is_removable=is_removable,
|
|
1228
|
+
is_renamable=is_renamable,
|
|
1229
|
+
source_entry_id=source_entry_id,
|
|
1230
|
+
clear_source_entry_id=clear_source_entry_id,
|
|
1231
|
+
is_hidden=is_hidden,
|
|
1232
|
+
is_static_view=is_static_view,
|
|
1233
|
+
is_shown_in_template=is_shown_in_template,
|
|
1234
|
+
template_item_fulfilled_timestamp=template_item_fulfilled_timestamp,
|
|
1235
|
+
clear_template_item_fulfilled_timestamp=clear_template_item_fulfilled_timestamp,
|
|
1236
|
+
entry_options_map=entry_options_map)
|
|
1237
|
+
self.force_step_update(step, update)
|
|
1238
|
+
|
|
1239
|
+
def force_step_update(self, step: Step, update: AbstractElnEntryUpdateCriteria) -> None:
|
|
1240
|
+
"""
|
|
1241
|
+
Immediately sent an update to an entry in this experiment. All changes will be reflected by the ExperimentEntry
|
|
1242
|
+
of the Step that is being updated.
|
|
1243
|
+
|
|
1244
|
+
Consider using store_step_update and commit_step_updates instead if the update does not need to be immediate.
|
|
1245
|
+
|
|
1246
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1247
|
+
list of steps in the experiment and caches them.
|
|
1248
|
+
|
|
1249
|
+
:param step: The step to update.
|
|
1250
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1251
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1252
|
+
:param update: The update to make to the step.
|
|
1253
|
+
"""
|
|
1254
|
+
step = self.__to_eln_step(step)
|
|
1255
|
+
self._eln_man.update_experiment_entry(self._exp_id, step.get_id(), update)
|
|
1256
|
+
self._update_entry_details(step, update)
|
|
1257
|
+
|
|
1258
|
+
def store_step_update(self, step: Step, update: AbstractElnEntryUpdateCriteria) -> None:
|
|
1259
|
+
"""
|
|
1260
|
+
Store updates to be made to an entry in this experiment. The updates are not committed until
|
|
1261
|
+
commit_entry_updates is called.
|
|
1262
|
+
|
|
1263
|
+
If the same entry is updated multiple times before committing, the latest update will be merged on top of the
|
|
1264
|
+
previous updates; where the new update and old update conflict, the new update will take precedence.
|
|
1265
|
+
|
|
1266
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1267
|
+
list of steps in the experiment and caches them.
|
|
1268
|
+
|
|
1269
|
+
:param step: The step to update.
|
|
1270
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1271
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1272
|
+
:param update: The update to make to the step.
|
|
1273
|
+
"""
|
|
1274
|
+
step = self.__to_eln_step(step)
|
|
1275
|
+
if step.eln_entry.entry_type != update.entry_type:
|
|
1276
|
+
raise SapioException(f"The provided step and update criteria are not of the same entry type. "
|
|
1277
|
+
f"The step is of type {step.eln_entry.entry_type} and the update criteria is of type "
|
|
1278
|
+
f"{update.entry_type}.")
|
|
1279
|
+
if step.get_id() in self._step_updates:
|
|
1280
|
+
self._merge_updates(update, self._step_updates[step.get_id()])
|
|
1281
|
+
else:
|
|
1282
|
+
self._step_updates[step.get_id()] = update
|
|
1283
|
+
|
|
1284
|
+
def store_step_updates(self, updates: dict[Step, AbstractElnEntryUpdateCriteria]) -> None:
|
|
1285
|
+
"""
|
|
1286
|
+
Store updates to be made to multiple entries in this experiment. The updates are not committed until
|
|
1287
|
+
commit_entry_updates is called.
|
|
1288
|
+
|
|
1289
|
+
If the same entry is updated multiple times before committing, the latest update will be merged on top of the
|
|
1290
|
+
previous updates; where the new update and old update conflict, the new update will take precedence.
|
|
1291
|
+
|
|
1292
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1293
|
+
list of steps in the experiment and caches them.
|
|
1294
|
+
|
|
1295
|
+
:param updates: A dictionary of steps and their respective updates.
|
|
1296
|
+
"""
|
|
1297
|
+
for step, update in updates.items():
|
|
1298
|
+
self.store_step_update(step, update)
|
|
1299
|
+
|
|
1300
|
+
def commit_step_updates(self) -> None:
|
|
1301
|
+
"""
|
|
1302
|
+
Commit all the stored updates to the entries in this experiment. The updates are made in the order that they
|
|
1303
|
+
were stored.
|
|
1304
|
+
"""
|
|
1305
|
+
self._eln_man.update_experiment_entries(self._exp_id, self._step_updates)
|
|
1306
|
+
for step_id, criteria in self._step_updates.items():
|
|
1307
|
+
self._update_entry_details(self._steps_by_id[step_id], criteria)
|
|
1308
|
+
self._step_updates.clear()
|
|
1309
|
+
|
|
1310
|
+
def _criteria_from_params(self, step: Step, *,
|
|
1311
|
+
entry_name: str | None = None,
|
|
1312
|
+
related_entry_set: Iterable[int] | None = None,
|
|
1313
|
+
dependency_set: Iterable[int] | None = None,
|
|
1314
|
+
entry_status: ExperimentEntryStatus | None = None,
|
|
1315
|
+
order: int | None = None,
|
|
1316
|
+
description: str | None = None,
|
|
1317
|
+
requires_grabber_plugin: bool | None = None,
|
|
1318
|
+
is_initialization_required: bool | None = None,
|
|
1319
|
+
notebook_experiment_tab_id: int | None = None,
|
|
1320
|
+
entry_height: int | None = None,
|
|
1321
|
+
column_order: int | None = None,
|
|
1322
|
+
column_span: int | None = None,
|
|
1323
|
+
is_removable: bool | None = None,
|
|
1324
|
+
is_renamable: bool | None = None,
|
|
1325
|
+
source_entry_id: int | None = None,
|
|
1326
|
+
clear_source_entry_id: bool | None = None,
|
|
1327
|
+
is_hidden: bool | None = None,
|
|
1328
|
+
is_static_view: bool | None = None,
|
|
1329
|
+
is_shown_in_template: bool | None = None,
|
|
1330
|
+
template_item_fulfilled_timestamp: int | None = None,
|
|
1331
|
+
clear_template_item_fulfilled_timestamp: bool | None = None,
|
|
1332
|
+
entry_options_map: dict[str, str] | None = None) -> AbstractElnEntryUpdateCriteria:
|
|
1333
|
+
"""
|
|
1334
|
+
Create an abstract update criteria object from the provided parameters for the given step.
|
|
1335
|
+
"""
|
|
929
1336
|
step: ElnEntryStep = self.__to_eln_step(step)
|
|
930
|
-
|
|
1337
|
+
update = AbstractElnEntryUpdateCriteria(step.eln_entry.entry_type)
|
|
931
1338
|
|
|
932
1339
|
# These two variables could be iterables that aren't lists. Convert them to plain
|
|
933
1340
|
# lists, since that's what the update criteria is expecting.
|
|
@@ -936,81 +1343,154 @@ class ExperimentHandler:
|
|
|
936
1343
|
if dependency_set is not None:
|
|
937
1344
|
dependency_set = list(dependency_set)
|
|
938
1345
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1346
|
+
update.entry_name = entry_name
|
|
1347
|
+
update.related_entry_set = related_entry_set
|
|
1348
|
+
update.dependency_set = dependency_set
|
|
1349
|
+
update.entry_status = entry_status
|
|
1350
|
+
update.order = order
|
|
1351
|
+
update.description = description
|
|
1352
|
+
update.requires_grabber_plugin = requires_grabber_plugin
|
|
1353
|
+
update.is_initialization_required = is_initialization_required
|
|
1354
|
+
update.notebook_experiment_tab_id = notebook_experiment_tab_id
|
|
1355
|
+
update.entry_height = entry_height
|
|
1356
|
+
update.column_order = column_order
|
|
1357
|
+
update.column_span = column_span
|
|
1358
|
+
update.is_removable = is_removable
|
|
1359
|
+
update.is_renamable = is_renamable
|
|
1360
|
+
update.source_entry_id = source_entry_id
|
|
1361
|
+
update.clear_source_entry_id = clear_source_entry_id
|
|
1362
|
+
update.is_hidden = is_hidden
|
|
1363
|
+
update.is_static_View = is_static_view
|
|
1364
|
+
update.is_shown_in_template = is_shown_in_template
|
|
1365
|
+
update.template_item_fulfilled_timestamp = template_item_fulfilled_timestamp
|
|
1366
|
+
update.clear_template_item_fulfilled_timestamp = clear_template_item_fulfilled_timestamp
|
|
1367
|
+
update.entry_options_map = entry_options_map
|
|
1368
|
+
|
|
1369
|
+
return update
|
|
1370
|
+
|
|
1371
|
+
@staticmethod
|
|
1372
|
+
def _merge_updates(new_update: AbstractElnEntryUpdateCriteria, old_update: AbstractElnEntryUpdateCriteria) -> None:
|
|
1373
|
+
"""
|
|
1374
|
+
Merge the new update criteria onto the old update criteria. The new update will take precedence where there
|
|
1375
|
+
are conflicts.
|
|
1376
|
+
"""
|
|
1377
|
+
for key, value in new_update.__dict__.items():
|
|
1378
|
+
if value is not None:
|
|
1379
|
+
old_update.__dict__[key] = value
|
|
1380
|
+
|
|
1381
|
+
def _update_entry_details(self, step: Step, update: AbstractElnEntryUpdateCriteria) -> None:
|
|
1382
|
+
"""
|
|
1383
|
+
Update the cached information for this entry in case it's needed by the caller after updating.
|
|
1384
|
+
"""
|
|
965
1385
|
entry: ExperimentEntry = step.eln_entry
|
|
966
|
-
if entry_name is not None:
|
|
1386
|
+
if update.entry_name is not None:
|
|
967
1387
|
# PR-46477 - Ensure that the previous name of the updated entry already existed in the cache.
|
|
968
|
-
if entry.entry_name in self.
|
|
969
|
-
self.
|
|
970
|
-
entry.entry_name = entry_name
|
|
971
|
-
self.
|
|
972
|
-
if related_entry_set is not None:
|
|
973
|
-
entry.related_entry_id_set = related_entry_set
|
|
974
|
-
if dependency_set is not None:
|
|
975
|
-
entry.dependency_set = dependency_set
|
|
976
|
-
if entry_status is not None:
|
|
977
|
-
entry.entry_status = entry_status
|
|
978
|
-
if order is not None:
|
|
979
|
-
entry.order = order
|
|
980
|
-
if description is not None:
|
|
981
|
-
entry.description = description
|
|
982
|
-
if requires_grabber_plugin is not None:
|
|
983
|
-
entry.requires_grabber_plugin = requires_grabber_plugin
|
|
984
|
-
if is_initialization_required is not None:
|
|
985
|
-
entry.is_initialization_required = is_initialization_required
|
|
986
|
-
if notebook_experiment_tab_id is not None:
|
|
987
|
-
entry.notebook_experiment_tab_id = notebook_experiment_tab_id
|
|
988
|
-
if entry_height is not None:
|
|
989
|
-
entry.entry_height = entry_height
|
|
990
|
-
if column_order is not None:
|
|
991
|
-
entry.column_order = column_order
|
|
992
|
-
if column_span is not None:
|
|
993
|
-
entry.column_span = column_span
|
|
994
|
-
if is_removable is not None:
|
|
995
|
-
entry.is_removable = is_removable
|
|
996
|
-
if is_renamable is not None:
|
|
997
|
-
entry.is_renamable = is_renamable
|
|
998
|
-
if source_entry_id is not None:
|
|
999
|
-
entry.source_entry_id = source_entry_id
|
|
1000
|
-
if clear_source_entry_id is True:
|
|
1388
|
+
if entry.entry_name in self._steps_by_name:
|
|
1389
|
+
self._steps_by_name.pop(entry.entry_name)
|
|
1390
|
+
entry.entry_name = update.entry_name
|
|
1391
|
+
self._steps_by_name.update({update.entry_name: step})
|
|
1392
|
+
if update.related_entry_set is not None:
|
|
1393
|
+
entry.related_entry_id_set = update.related_entry_set
|
|
1394
|
+
if update.dependency_set is not None:
|
|
1395
|
+
entry.dependency_set = update.dependency_set
|
|
1396
|
+
if update.entry_status is not None:
|
|
1397
|
+
entry.entry_status = update.entry_status
|
|
1398
|
+
if update.order is not None:
|
|
1399
|
+
entry.order = update.order
|
|
1400
|
+
if update.description is not None:
|
|
1401
|
+
entry.description = update.description
|
|
1402
|
+
if update.requires_grabber_plugin is not None:
|
|
1403
|
+
entry.requires_grabber_plugin = update.requires_grabber_plugin
|
|
1404
|
+
if update.is_initialization_required is not None:
|
|
1405
|
+
entry.is_initialization_required = update.is_initialization_required
|
|
1406
|
+
if update.notebook_experiment_tab_id is not None:
|
|
1407
|
+
entry.notebook_experiment_tab_id = update.notebook_experiment_tab_id
|
|
1408
|
+
if update.entry_height is not None:
|
|
1409
|
+
entry.entry_height = update.entry_height
|
|
1410
|
+
if update.column_order is not None:
|
|
1411
|
+
entry.column_order = update.column_order
|
|
1412
|
+
if update.column_span is not None:
|
|
1413
|
+
entry.column_span = update.column_span
|
|
1414
|
+
if update.is_removable is not None:
|
|
1415
|
+
entry.is_removable = update.is_removable
|
|
1416
|
+
if update.is_renamable is not None:
|
|
1417
|
+
entry.is_renamable = update.is_renamable
|
|
1418
|
+
if update.source_entry_id is not None:
|
|
1419
|
+
entry.source_entry_id = update.source_entry_id
|
|
1420
|
+
if update.clear_source_entry_id is True:
|
|
1001
1421
|
entry.source_entry_id = None
|
|
1002
|
-
if is_hidden is not None:
|
|
1003
|
-
entry.is_hidden = is_hidden
|
|
1004
|
-
if is_static_View is not None:
|
|
1005
|
-
entry.is_static_View = is_static_View
|
|
1006
|
-
if is_shown_in_template is not None:
|
|
1007
|
-
entry.is_shown_in_template = is_shown_in_template
|
|
1008
|
-
if template_item_fulfilled_timestamp is not None:
|
|
1009
|
-
entry.template_item_fulfilled_timestamp = template_item_fulfilled_timestamp
|
|
1010
|
-
if clear_template_item_fulfilled_timestamp is True:
|
|
1422
|
+
if update.is_hidden is not None:
|
|
1423
|
+
entry.is_hidden = update.is_hidden
|
|
1424
|
+
if update.is_static_View is not None:
|
|
1425
|
+
entry.is_static_View = update.is_static_View
|
|
1426
|
+
if update.is_shown_in_template is not None:
|
|
1427
|
+
entry.is_shown_in_template = update.is_shown_in_template
|
|
1428
|
+
if update.template_item_fulfilled_timestamp is not None:
|
|
1429
|
+
entry.template_item_fulfilled_timestamp = update.template_item_fulfilled_timestamp
|
|
1430
|
+
if update.clear_template_item_fulfilled_timestamp is True:
|
|
1011
1431
|
entry.template_item_fulfilled_timestamp = None
|
|
1012
|
-
if entry_options_map is not None:
|
|
1013
|
-
self.
|
|
1432
|
+
if update.entry_options_map is not None:
|
|
1433
|
+
self._step_options.update({step.get_id(): update.entry_options_map})
|
|
1434
|
+
|
|
1435
|
+
if isinstance(entry, ExperimentAttachmentEntry) and isinstance(update, ElnAttachmentEntryUpdateCriteria):
|
|
1436
|
+
if update.entry_attachment_list is not None:
|
|
1437
|
+
entry.entry_attachment_list = update.entry_attachment_list
|
|
1438
|
+
if update.record_id is not None:
|
|
1439
|
+
entry.record_id = update.record_id
|
|
1440
|
+
if update.attachment_name is not None:
|
|
1441
|
+
entry.attachment_name = update.attachment_name
|
|
1442
|
+
elif isinstance(entry, ExperimentDashboardEntry) and isinstance(update, ElnDashboardEntryUpdateCriteria):
|
|
1443
|
+
if update.dashboard_guid is not None:
|
|
1444
|
+
entry.dashboard_guid = update.dashboard_guid
|
|
1445
|
+
if update.dashboard_guid_list is not None:
|
|
1446
|
+
entry.dashboard_guid_list = update.dashboard_guid_list
|
|
1447
|
+
if update.data_source_entry_id is not None:
|
|
1448
|
+
entry.data_source_entry_id = update.data_source_entry_id
|
|
1449
|
+
elif isinstance(entry, ExperimentFormEntry) and isinstance(update, ElnFormEntryUpdateCriteria):
|
|
1450
|
+
if update.record_id is not None:
|
|
1451
|
+
entry.record_id = update.record_id
|
|
1452
|
+
if update.form_name_list is not None:
|
|
1453
|
+
entry.form_name_list = update.form_name_list
|
|
1454
|
+
if update.data_type_layout_name is not None:
|
|
1455
|
+
entry.data_type_layout_name = update.data_type_layout_name
|
|
1456
|
+
if update.field_set_id_list is not None:
|
|
1457
|
+
entry.field_set_id_list = update.field_set_id_list
|
|
1458
|
+
if update.extension_type_list is not None:
|
|
1459
|
+
entry.extension_type_list = update.extension_type_list
|
|
1460
|
+
if update.data_field_name_list is not None:
|
|
1461
|
+
entry.data_field_name_list = update.data_field_name_list
|
|
1462
|
+
if update.is_existing_field_removable is not None:
|
|
1463
|
+
entry.is_existing_field_removable = update.is_existing_field_removable
|
|
1464
|
+
if update.is_field_addable is not None:
|
|
1465
|
+
entry.is_field_addable = update.is_field_addable
|
|
1466
|
+
elif isinstance(entry, ExperimentPluginEntry) and isinstance(update, ElnPluginEntryUpdateCriteria):
|
|
1467
|
+
if update.plugin_name is not None:
|
|
1468
|
+
entry.plugin_name = update.plugin_name
|
|
1469
|
+
if update.provides_template_data is not None:
|
|
1470
|
+
entry.provides_template_data = update.provides_template_data
|
|
1471
|
+
if update.using_template_data is not None:
|
|
1472
|
+
entry.using_template_data = update.using_template_data
|
|
1473
|
+
if isinstance(entry, ExperimentTableEntry) and isinstance(update, ElnTableEntryUpdateCriteria):
|
|
1474
|
+
if update.data_type_layout_name is not None:
|
|
1475
|
+
entry.data_type_layout_name = update.data_type_layout_name
|
|
1476
|
+
if update.extension_type_list is not None:
|
|
1477
|
+
entry.extension_type_list = update.extension_type_list
|
|
1478
|
+
if update.field_set_id_list is not None:
|
|
1479
|
+
entry.field_set_id_list = update.field_set_id_list
|
|
1480
|
+
if update.is_existing_field_removable is not None:
|
|
1481
|
+
entry.is_existing_field_removable = update.is_existing_field_removable
|
|
1482
|
+
if update.is_field_addable is not None:
|
|
1483
|
+
entry.is_field_addable = update.is_field_addable
|
|
1484
|
+
if update.show_key_fields is not None:
|
|
1485
|
+
entry.show_key_fields = update.show_key_fields
|
|
1486
|
+
if update.table_column_list is not None:
|
|
1487
|
+
entry.table_column_list = update.table_column_list
|
|
1488
|
+
elif isinstance(entry, ExperimentTempDataEntry) and isinstance(update, ElnTempDataEntryUpdateCriteria):
|
|
1489
|
+
if update.plugin_path is not None:
|
|
1490
|
+
entry.plugin_path = update.plugin_path
|
|
1491
|
+
elif isinstance(entry, ExperimentTextEntry) and isinstance(update, ElnTextEntryUpdateCriteria):
|
|
1492
|
+
# Text update criteria has no additional fields.
|
|
1493
|
+
pass
|
|
1014
1494
|
|
|
1015
1495
|
def get_step_option(self, step: Step, option: str) -> str:
|
|
1016
1496
|
"""
|
|
@@ -1050,9 +1530,10 @@ class ExperimentHandler:
|
|
|
1050
1530
|
:return: The map of options for the input step.
|
|
1051
1531
|
"""
|
|
1052
1532
|
step = self.__to_eln_step(step)
|
|
1053
|
-
if step not in self.
|
|
1054
|
-
self.
|
|
1055
|
-
|
|
1533
|
+
if step not in self._step_options:
|
|
1534
|
+
self._step_options.update(ExperimentReportUtil.get_experiment_entry_options(self.user,
|
|
1535
|
+
self.get_all_steps()))
|
|
1536
|
+
return self._step_options[step.get_id()]
|
|
1056
1537
|
|
|
1057
1538
|
def add_step_options(self, step: Step, mapping: Mapping[str, str]):
|
|
1058
1539
|
"""
|
|
@@ -1076,7 +1557,7 @@ class ExperimentHandler:
|
|
|
1076
1557
|
"""
|
|
1077
1558
|
options: dict[str, str] = self.get_step_options(step)
|
|
1078
1559
|
options.update(mapping)
|
|
1079
|
-
self.
|
|
1560
|
+
self.force_step_update_params(step, entry_options_map=options)
|
|
1080
1561
|
|
|
1081
1562
|
def initialize_step(self, step: Step) -> None:
|
|
1082
1563
|
"""
|
|
@@ -1094,7 +1575,7 @@ class ExperimentHandler:
|
|
|
1094
1575
|
# Avoid unnecessary calls if the step is already initialized.
|
|
1095
1576
|
step: ElnEntryStep = self.__to_eln_step(step)
|
|
1096
1577
|
if step.eln_entry.template_item_fulfilled_timestamp is None:
|
|
1097
|
-
self.
|
|
1578
|
+
self.force_step_update_params(step, template_item_fulfilled_timestamp=TimeUtil.now_in_millis())
|
|
1098
1579
|
|
|
1099
1580
|
def uninitialize_step(self, step: Step) -> None:
|
|
1100
1581
|
"""
|
|
@@ -1112,7 +1593,7 @@ class ExperimentHandler:
|
|
|
1112
1593
|
# Avoid unnecessary calls if the step is already uninitialized.
|
|
1113
1594
|
step: ElnEntryStep = self.__to_eln_step(step)
|
|
1114
1595
|
if step.eln_entry.template_item_fulfilled_timestamp is not None:
|
|
1115
|
-
self.
|
|
1596
|
+
self.force_step_update_params(step, clear_template_item_fulfilled_timestamp=True)
|
|
1116
1597
|
|
|
1117
1598
|
def complete_step(self, step: Step) -> None:
|
|
1118
1599
|
"""
|
|
@@ -1128,7 +1609,7 @@ class ExperimentHandler:
|
|
|
1128
1609
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1129
1610
|
"""
|
|
1130
1611
|
step = self.__to_eln_step(step)
|
|
1131
|
-
if step.eln_entry.entry_status not in self.
|
|
1612
|
+
if step.eln_entry.entry_status not in self._ENTRY_COMPLETE_STATUSES:
|
|
1132
1613
|
step.complete_step()
|
|
1133
1614
|
step.eln_entry.entry_status = ExperimentEntryStatus.Completed
|
|
1134
1615
|
|
|
@@ -1146,7 +1627,7 @@ class ExperimentHandler:
|
|
|
1146
1627
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1147
1628
|
"""
|
|
1148
1629
|
step = self.__to_eln_step(step)
|
|
1149
|
-
if step.eln_entry.entry_status in self.
|
|
1630
|
+
if step.eln_entry.entry_status in self._ENTRY_LOCKED_STATUSES:
|
|
1150
1631
|
step.unlock_step()
|
|
1151
1632
|
step.eln_entry.entry_status = ExperimentEntryStatus.UnlockedChangesRequired
|
|
1152
1633
|
|
|
@@ -1168,8 +1649,8 @@ class ExperimentHandler:
|
|
|
1168
1649
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1169
1650
|
"""
|
|
1170
1651
|
step = self.__to_eln_step(step)
|
|
1171
|
-
if step.eln_entry.entry_status in self.
|
|
1172
|
-
self.
|
|
1652
|
+
if step.eln_entry.entry_status in self._ENTRY_LOCKED_STATUSES:
|
|
1653
|
+
self.force_step_update_params(step, entry_status=ExperimentEntryStatus.Disabled)
|
|
1173
1654
|
|
|
1174
1655
|
def step_is_submitted(self, step: Step) -> bool:
|
|
1175
1656
|
"""
|
|
@@ -1184,7 +1665,7 @@ class ExperimentHandler:
|
|
|
1184
1665
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1185
1666
|
:return: True if the step's status is Completed or CompletedApproved. False otherwise.
|
|
1186
1667
|
"""
|
|
1187
|
-
return self.__to_eln_step(step).eln_entry.entry_status in self.
|
|
1668
|
+
return self.__to_eln_step(step).eln_entry.entry_status in self._ENTRY_COMPLETE_STATUSES
|
|
1188
1669
|
|
|
1189
1670
|
def step_is_locked(self, step: Step) -> bool:
|
|
1190
1671
|
"""
|
|
@@ -1200,7 +1681,171 @@ class ExperimentHandler:
|
|
|
1200
1681
|
:return: True if the step's status is Completed, CompletedApproved, Disabled, LockedAwaitingApproval,
|
|
1201
1682
|
or LockedRejected. False otherwise.
|
|
1202
1683
|
"""
|
|
1203
|
-
return self.__to_eln_step(step).eln_entry.entry_status in self.
|
|
1684
|
+
return self.__to_eln_step(step).eln_entry.entry_status in self._ENTRY_LOCKED_STATUSES
|
|
1685
|
+
|
|
1686
|
+
# FR-47464: Some functions that can help with entry placement.
|
|
1687
|
+
def get_all_tabs(self) -> list[ElnExperimentTab]:
|
|
1688
|
+
"""
|
|
1689
|
+
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1690
|
+
list of tabs in the experiment and caches them.
|
|
1691
|
+
|
|
1692
|
+
:return: A list of all the tabs in the experiment in order of appearance.
|
|
1693
|
+
"""
|
|
1694
|
+
if not self._queried_all_tabs:
|
|
1695
|
+
self._tabs = self._eln_man.get_tabs_for_experiment(self._exp_id)
|
|
1696
|
+
self._tabs.sort(key=lambda t: t.tab_order)
|
|
1697
|
+
self._tabs_by_id = {tab.tab_id: tab for tab in self._tabs}
|
|
1698
|
+
self._tabs_by_name = {tab.tab_name: tab for tab in self._tabs}
|
|
1699
|
+
return self._tabs
|
|
1700
|
+
|
|
1701
|
+
def get_first_tab(self) -> ElnExperimentTab:
|
|
1702
|
+
"""
|
|
1703
|
+
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1704
|
+
list of tabs in the experiment and caches them.
|
|
1705
|
+
|
|
1706
|
+
:return: The first tab in the experiment.
|
|
1707
|
+
"""
|
|
1708
|
+
return self.get_all_tabs()[0]
|
|
1709
|
+
|
|
1710
|
+
def get_last_tab(self) -> ElnExperimentTab:
|
|
1711
|
+
"""
|
|
1712
|
+
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1713
|
+
list of tabs in the experiment and caches them.
|
|
1714
|
+
|
|
1715
|
+
:return: The last tab in the experiment.
|
|
1716
|
+
"""
|
|
1717
|
+
return self.get_all_tabs()[-1]
|
|
1718
|
+
|
|
1719
|
+
def create_tab(self, tab_name: str) -> ElnExperimentTab:
|
|
1720
|
+
"""
|
|
1721
|
+
Create a new tab in the experiment with the input name.
|
|
1722
|
+
|
|
1723
|
+
:param tab_name: The name of the tab to create.
|
|
1724
|
+
:return: The newly created tab.
|
|
1725
|
+
"""
|
|
1726
|
+
crit = ElnExperimentTabAddCriteria(tab_name, [])
|
|
1727
|
+
tab: ElnExperimentTab = self._eln_man.add_tab_for_experiment(self._exp_id, crit)
|
|
1728
|
+
self.add_tab_to_cache(tab)
|
|
1729
|
+
return tab
|
|
1730
|
+
|
|
1731
|
+
def get_tab(self, tab_name: str | int, exception_on_none: bool = True) -> ElnExperimentTab:
|
|
1732
|
+
"""
|
|
1733
|
+
Return the tab with the input name.
|
|
1734
|
+
|
|
1735
|
+
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1736
|
+
list of tabs in the experiment and caches them.
|
|
1737
|
+
|
|
1738
|
+
:param tab_name: The name or ID of the tab to get.
|
|
1739
|
+
:param exception_on_none: If True, raises an exception if no tab with the given name exists.
|
|
1740
|
+
:return: The tab with the input name, or None if no such tab exists.
|
|
1741
|
+
"""
|
|
1742
|
+
if tab_name not in self._tabs_by_name and tab_name not in self._tabs_by_id:
|
|
1743
|
+
self.get_all_tabs()
|
|
1744
|
+
if isinstance(tab_name, str):
|
|
1745
|
+
tab = self._tabs_by_name.get(tab_name)
|
|
1746
|
+
else:
|
|
1747
|
+
tab = self._tabs_by_id.get(tab_name)
|
|
1748
|
+
if tab is None and exception_on_none:
|
|
1749
|
+
raise SapioException(f"No tab with the name\\ID \"{tab_name}\" exists in this experiment.")
|
|
1750
|
+
return tab
|
|
1751
|
+
|
|
1752
|
+
def get_steps_in_tab(self, tab: TabIdentifier | str, data_type: DataTypeIdentifier | None = None) \
|
|
1753
|
+
-> list[ElnEntryStep]:
|
|
1754
|
+
"""
|
|
1755
|
+
Get all the steps in the input tab sorted in order of appearance.
|
|
1756
|
+
|
|
1757
|
+
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1758
|
+
list of tabs in the experiment and caches them.
|
|
1759
|
+
|
|
1760
|
+
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1761
|
+
and caches them.
|
|
1762
|
+
|
|
1763
|
+
:param tab: The tab to get the steps of. This can be either an ElnExperimentTab object, or the name or ID of
|
|
1764
|
+
the tab.
|
|
1765
|
+
:param data_type: The data type to filter the steps by. If None, all steps are returned.
|
|
1766
|
+
:return: A list of all the steps in the input tab sorted in order of appearance.
|
|
1767
|
+
"""
|
|
1768
|
+
tab: ElnExperimentTab = self.__to_eln_tab(tab)
|
|
1769
|
+
steps: list[ElnEntryStep] = []
|
|
1770
|
+
for step in self.get_all_steps(data_type):
|
|
1771
|
+
if step.eln_entry.notebook_experiment_tab_id == tab.tab_id:
|
|
1772
|
+
steps.append(step)
|
|
1773
|
+
return steps
|
|
1774
|
+
|
|
1775
|
+
def get_next_entry_order_in_tab(self, tab: TabIdentifier | str) -> int:
|
|
1776
|
+
"""
|
|
1777
|
+
Get the next available order for a new entry in the input tab.
|
|
1778
|
+
|
|
1779
|
+
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1780
|
+
list of tabs in the experiment and caches them.
|
|
1781
|
+
|
|
1782
|
+
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1783
|
+
and caches them.
|
|
1784
|
+
|
|
1785
|
+
:param tab: The tab to get the next entry order of. This can be either an ElnExperimentTab object, or the name
|
|
1786
|
+
or ID of the tab.
|
|
1787
|
+
:return: The next available order for a new entry in the input tab.
|
|
1788
|
+
"""
|
|
1789
|
+
steps = self.get_steps_in_tab(tab)
|
|
1790
|
+
return steps[-1].eln_entry.order + 1 if steps else 0
|
|
1791
|
+
|
|
1792
|
+
# FR-47530: Add functions for dealing with entry positioning.
|
|
1793
|
+
def step_to_position(self, step: Step) -> ElnEntryPosition:
|
|
1794
|
+
"""
|
|
1795
|
+
Get the position of the input step in the experiment.
|
|
1796
|
+
|
|
1797
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1798
|
+
list of steps in the experiment and caches them.
|
|
1799
|
+
|
|
1800
|
+
:param step:
|
|
1801
|
+
The step to get the position of.
|
|
1802
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1803
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1804
|
+
:return: The position of the input step in the experiment.
|
|
1805
|
+
"""
|
|
1806
|
+
step: ElnEntryStep = self.__to_eln_step(step)
|
|
1807
|
+
entry: ExperimentEntry = step.eln_entry
|
|
1808
|
+
return ElnEntryPosition(entry.notebook_experiment_tab_id,
|
|
1809
|
+
entry.order,
|
|
1810
|
+
entry.column_span,
|
|
1811
|
+
entry.column_order)
|
|
1812
|
+
|
|
1813
|
+
def step_at_position(self, position: ElnEntryPosition) -> Step | None:
|
|
1814
|
+
"""
|
|
1815
|
+
Get the step at the input position in the experiment.
|
|
1816
|
+
|
|
1817
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1818
|
+
list of steps in the experiment and caches them.
|
|
1819
|
+
|
|
1820
|
+
:param position: The position to get the step at.
|
|
1821
|
+
:return: The step at the input position in the experiment, or None if no step exists at that position.
|
|
1822
|
+
"""
|
|
1823
|
+
if position.tab_id is None or position.order is None:
|
|
1824
|
+
raise SapioException("The provided position must at least have a tab ID and order.")
|
|
1825
|
+
for step in self.get_steps_in_tab(position.tab_id):
|
|
1826
|
+
entry: ExperimentEntry = step.eln_entry
|
|
1827
|
+
if entry.order != position.order:
|
|
1828
|
+
continue
|
|
1829
|
+
if position.column_span is not None and entry.column_span != position.column_span:
|
|
1830
|
+
continue
|
|
1831
|
+
if position.column_order is not None and entry.column_order != position.column_order:
|
|
1832
|
+
continue
|
|
1833
|
+
return step
|
|
1834
|
+
return None
|
|
1835
|
+
|
|
1836
|
+
# FR-47530: Create a function for adding protocol templates to the experiment.
|
|
1837
|
+
def add_protocol(self, protocol: ProtocolTemplateInfo | int, position: ElnEntryPosition) -> list[ElnEntryStep]:
|
|
1838
|
+
"""
|
|
1839
|
+
Add a protocol to the experiment. Updates the handler cache with the newly created entries.
|
|
1840
|
+
|
|
1841
|
+
:param protocol: The protocol to add. This can be either a ProtocolTemplateInfo object or the ID of the
|
|
1842
|
+
protocol template.
|
|
1843
|
+
:param position: The position that the protocol's first entry will be placed at.
|
|
1844
|
+
:return: The newly created protocol entries.
|
|
1845
|
+
"""
|
|
1846
|
+
protocol = protocol if isinstance(protocol, int) else protocol.template_id
|
|
1847
|
+
new_entries: list[ExperimentEntry] = self._eln_man.add_protocol_template(self._exp_id, protocol, position)
|
|
1848
|
+
return self.add_entries_to_caches(new_entries)
|
|
1204
1849
|
|
|
1205
1850
|
def __to_eln_step(self, step: Step) -> ElnEntryStep:
|
|
1206
1851
|
"""
|
|
@@ -1210,15 +1855,24 @@ class ExperimentHandler:
|
|
|
1210
1855
|
|
|
1211
1856
|
:return: The input step as an ElnEntryStep.
|
|
1212
1857
|
"""
|
|
1213
|
-
|
|
1858
|
+
if isinstance(step, str):
|
|
1859
|
+
return self.get_step(step)
|
|
1860
|
+
if isinstance(step, int):
|
|
1861
|
+
return self._steps_by_id.get(step)
|
|
1862
|
+
if isinstance(step, ExperimentEntry):
|
|
1863
|
+
return self.add_entry_to_caches(step)
|
|
1864
|
+
return step
|
|
1214
1865
|
|
|
1215
|
-
def
|
|
1866
|
+
def __to_eln_tab(self, tab: TabIdentifier | str) -> ElnExperimentTab:
|
|
1216
1867
|
"""
|
|
1217
|
-
|
|
1868
|
+
Convert a variable that could be either a string or an ElnExperimentTab to just an ElnExperimentTab.
|
|
1869
|
+
This will query and cache the tabs for the experiment if the input tab is a name and the tabs have not been
|
|
1870
|
+
cached before.
|
|
1218
1871
|
|
|
1219
|
-
:return: The
|
|
1872
|
+
:return: The input tab as an ElnExperimentTab.
|
|
1220
1873
|
"""
|
|
1221
|
-
if
|
|
1222
|
-
return self.
|
|
1223
|
-
|
|
1224
|
-
|
|
1874
|
+
if isinstance(tab, str):
|
|
1875
|
+
return self.get_tab(tab)
|
|
1876
|
+
if isinstance(tab, int):
|
|
1877
|
+
return [x for x in self._tabs if x.tab_id == tab][0]
|
|
1878
|
+
return tab
|