sapiopycommons 2025.3.21a458__py3-none-any.whl → 2025.3.25a459__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/tool_of_tools.py +917 -0
- sapiopycommons/callbacks/callback_util.py +25 -17
- sapiopycommons/customreport/auto_pagers.py +28 -18
- sapiopycommons/datatype/attachment_util.py +4 -2
- sapiopycommons/datatype/data_fields.py +22 -0
- sapiopycommons/eln/experiment_handler.py +1112 -184
- sapiopycommons/eln/experiment_report_util.py +8 -3
- sapiopycommons/eln/experiment_tags.py +7 -0
- sapiopycommons/eln/plate_designer.py +159 -59
- sapiopycommons/general/html_formatter.py +456 -0
- sapiopycommons/general/sapio_links.py +12 -4
- sapiopycommons/processtracking/custom_workflow_handler.py +42 -27
- sapiopycommons/recordmodel/record_handler.py +187 -130
- sapiopycommons/rules/eln_rule_handler.py +33 -29
- sapiopycommons/rules/on_save_rule_handler.py +33 -29
- {sapiopycommons-2025.3.21a458.dist-info → sapiopycommons-2025.3.25a459.dist-info}/METADATA +1 -1
- {sapiopycommons-2025.3.21a458.dist-info → sapiopycommons-2025.3.25a459.dist-info}/RECORD +20 -16
- {sapiopycommons-2025.3.21a458.dist-info → sapiopycommons-2025.3.25a459.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.3.21a458.dist-info → sapiopycommons-2025.3.25a459.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,90 +1,133 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import warnings
|
|
4
4
|
from collections.abc import Mapping, Iterable
|
|
5
5
|
from typing import TypeAlias
|
|
6
6
|
from weakref import WeakValueDictionary
|
|
7
7
|
|
|
8
8
|
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
9
|
+
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
9
10
|
from sapiopylib.rest.ELNService import ElnManager
|
|
10
11
|
from sapiopylib.rest.User import SapioUser
|
|
11
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
|
|
15
|
+
from sapiopylib.rest.pojo.eln.ElnEntryPosition import ElnEntryPosition
|
|
12
16
|
from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment, TemplateExperimentQueryPojo, ElnTemplate, \
|
|
13
17
|
InitializeNotebookExperimentPojo, ElnExperimentUpdateCriteria
|
|
14
|
-
from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry
|
|
15
|
-
|
|
18
|
+
from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry, ExperimentTableEntry, ExperimentFormEntry, \
|
|
19
|
+
ExperimentAttachmentEntry, ExperimentPluginEntry, ExperimentDashboardEntry, ExperimentTextEntry, \
|
|
20
|
+
ExperimentTempDataEntry, EntryAttachment, EntryRecordAttachment
|
|
21
|
+
from sapiopylib.rest.pojo.eln.ExperimentEntryCriteria import AbstractElnEntryUpdateCriteria, \
|
|
22
|
+
ElnTableEntryUpdateCriteria, ElnFormEntryUpdateCriteria, ElnAttachmentEntryUpdateCriteria, \
|
|
23
|
+
ElnPluginEntryUpdateCriteria, ElnDashboardEntryUpdateCriteria, ElnTextEntryUpdateCriteria, \
|
|
24
|
+
ElnTempDataEntryUpdateCriteria, ElnEntryCriteria
|
|
16
25
|
from sapiopylib.rest.pojo.eln.SapioELNEnums import ExperimentEntryStatus, ElnExperimentStatus, ElnEntryType, \
|
|
17
26
|
ElnBaseDataType
|
|
27
|
+
from sapiopylib.rest.pojo.eln.eln_headings import ElnExperimentTab, ElnExperimentTabAddCriteria
|
|
28
|
+
from sapiopylib.rest.pojo.eln.field_set import ElnFieldSetInfo
|
|
18
29
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
19
30
|
from sapiopylib.rest.pojo.webhook.WebhookDirective import ElnExperimentDirective
|
|
20
31
|
from sapiopylib.rest.pojo.webhook.WebhookResult import SapioWebhookResult
|
|
21
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
|
|
22
37
|
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
23
38
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelInstanceManager, RecordModelManager
|
|
24
39
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
25
40
|
from sapiopylib.rest.utils.recordmodel.properties import Child
|
|
26
41
|
|
|
42
|
+
from sapiopycommons.datatype.data_fields import SystemFields
|
|
27
43
|
from sapiopycommons.eln.experiment_report_util import ExperimentReportUtil
|
|
44
|
+
from sapiopycommons.eln.experiment_tags import PLATE_DESIGNER_PLUGIN
|
|
28
45
|
from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIdentifier, UserIdentifier, \
|
|
29
|
-
DataTypeIdentifier, RecordModel
|
|
46
|
+
DataTypeIdentifier, RecordModel, FieldMap, FieldIdentifier
|
|
30
47
|
from sapiopycommons.general.exceptions import SapioException
|
|
48
|
+
from sapiopycommons.general.time_util import TimeUtil
|
|
49
|
+
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
31
50
|
|
|
32
51
|
Step: TypeAlias = str | ElnEntryStep
|
|
33
52
|
"""An object representing an identifier to an ElnEntryStep. May be either the name of the step or the ElnEntryStep
|
|
34
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."""
|
|
35
60
|
|
|
36
61
|
|
|
37
62
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
38
63
|
class ExperimentHandler:
|
|
39
64
|
user: SapioUser
|
|
40
65
|
context: SapioWebhookContext | None
|
|
41
|
-
"""The context that this handler is working from."""
|
|
66
|
+
"""The context that this handler is working from, if any."""
|
|
42
67
|
|
|
68
|
+
# CR-47485: Made variables protected instead of private.
|
|
43
69
|
# Basic experiment info from the context.
|
|
44
|
-
|
|
70
|
+
_eln_exp: ElnExperiment
|
|
45
71
|
"""The ELN experiment from the context."""
|
|
46
|
-
|
|
72
|
+
_protocol: ElnExperimentProtocol
|
|
47
73
|
"""The ELN experiment as a protocol."""
|
|
48
|
-
|
|
74
|
+
_exp_id: int
|
|
49
75
|
"""The ID of this experiment's notebook. Used for making update webservice calls."""
|
|
50
76
|
|
|
51
77
|
# Managers.
|
|
52
|
-
|
|
78
|
+
_eln_man: ElnManager
|
|
53
79
|
"""The ELN manager. Used for updating the experiment and its steps."""
|
|
54
|
-
|
|
80
|
+
_inst_man: RecordModelInstanceManager
|
|
55
81
|
"""The record model instance manager. Used for wrapping the data records of a step as record models."""
|
|
82
|
+
_rec_handler: RecordHandler
|
|
83
|
+
"""The record handler. Also used for wrapping the data records of a step as record models."""
|
|
56
84
|
|
|
57
85
|
# Only a fraction of the information about the current experiment exists in the context. Much information requires
|
|
58
86
|
# additional queries to obtain, but may also be repeatedly accessed. In such cases, cache the information after it
|
|
59
87
|
# has been requested so that the user doesn't need to worry about caching it themselves.
|
|
60
88
|
# CR-46341: Replace class variables with instance variables.
|
|
61
|
-
|
|
89
|
+
_exp_record: DataRecord | None
|
|
62
90
|
"""The data record for this experiment. Only cached when first accessed."""
|
|
63
|
-
|
|
91
|
+
_exp_template: ElnTemplate | None
|
|
64
92
|
"""The template for this experiment. Only cached when first accessed."""
|
|
65
|
-
|
|
93
|
+
_exp_options: dict[str, str]
|
|
66
94
|
"""Experiment options for this experiment. Only cached when first accessed."""
|
|
67
95
|
|
|
68
|
-
|
|
96
|
+
_queried_all_steps: bool
|
|
69
97
|
"""Whether this ExperimentHandler has queried the system for all steps in the experiment."""
|
|
70
|
-
|
|
71
|
-
"""Steps from this experiment. All steps are cached the first time any individual step is accessed."""
|
|
72
|
-
|
|
98
|
+
_steps: dict[str, ElnEntryStep]
|
|
99
|
+
"""Steps from this experiment by their name. All steps are cached the first time any individual step is accessed."""
|
|
100
|
+
_steps_by_id: dict[int, ElnEntryStep]
|
|
101
|
+
"""Steps from this experiment by their ID. All steps are cached the first time any individual step is accessed."""
|
|
102
|
+
_step_options: dict[int, dict[str, str]]
|
|
73
103
|
"""Entry options for each step in this experiment. All entry options are cached the first time any individual step's
|
|
74
104
|
options are queried. The cache is updated whenever the entry options for a step are changed by this handler."""
|
|
75
105
|
|
|
106
|
+
_step_updates: dict[int, AbstractElnEntryUpdateCriteria]
|
|
107
|
+
"""A dictionary of entry updates that have been made by this handler. Used to batch update entries."""
|
|
108
|
+
|
|
109
|
+
_queried_all_tabs: bool
|
|
110
|
+
"""Whether this ExperimentHandler has queried the system for all tabs in the experiment."""
|
|
111
|
+
_tabs: list[ElnExperimentTab]
|
|
112
|
+
"""The tabs for this experiment. Only cached when first accessed."""
|
|
113
|
+
_tabs_by_name: dict[str, ElnExperimentTab]
|
|
114
|
+
"""The tabs for this experiment by their name. Only cached when first accessed."""
|
|
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
|
+
|
|
76
119
|
# Constants
|
|
77
|
-
|
|
120
|
+
_ENTRY_COMPLETE_STATUSES = [ExperimentEntryStatus.Completed, ExperimentEntryStatus.CompletedApproved]
|
|
78
121
|
"""The set of statuses that an ELN entry could have and be considered completed/submitted."""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
122
|
+
_ENTRY_LOCKED_STATUSES = [ExperimentEntryStatus.Completed, ExperimentEntryStatus.CompletedApproved,
|
|
123
|
+
ExperimentEntryStatus.Disabled, ExperimentEntryStatus.LockedAwaitingApproval,
|
|
124
|
+
ExperimentEntryStatus.LockedRejected]
|
|
82
125
|
"""The set of statuses that an ELN entry could have and be considered locked."""
|
|
83
|
-
|
|
126
|
+
_EXPERIMENT_COMPLETE_STATUSES = [ElnExperimentStatus.Completed, ElnExperimentStatus.CompletedApproved]
|
|
84
127
|
"""The set of statuses that an ELN experiment could have and be considered completed."""
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
128
|
+
_EXPERIMENT_LOCKED_STATUSES = [ElnExperimentStatus.Completed, ElnExperimentStatus.CompletedApproved,
|
|
129
|
+
ElnExperimentStatus.LockedRejected, ElnExperimentStatus.LockedAwaitingApproval,
|
|
130
|
+
ElnExperimentStatus.Canceled]
|
|
88
131
|
"""The set of statuses that an ELN experiment could have and be considered locked."""
|
|
89
132
|
|
|
90
133
|
__instances: WeakValueDictionary[str, ExperimentHandler] = WeakValueDictionary()
|
|
@@ -124,27 +167,40 @@ class ExperimentHandler:
|
|
|
124
167
|
experiment = param_results[2]
|
|
125
168
|
|
|
126
169
|
# Get the basic information about this experiment that already exists in the context and is often used.
|
|
127
|
-
self.
|
|
128
|
-
self.
|
|
129
|
-
self.
|
|
170
|
+
self._eln_exp = experiment
|
|
171
|
+
self._protocol = ElnExperimentProtocol(experiment, self.user)
|
|
172
|
+
self._exp_id = self._protocol.get_id()
|
|
130
173
|
|
|
131
174
|
# Grab various managers that may be used.
|
|
132
|
-
self.
|
|
133
|
-
self.
|
|
175
|
+
self._eln_man = DataMgmtServer.get_eln_manager(self.user)
|
|
176
|
+
self._inst_man = RecordModelManager(self.user).instance_manager
|
|
177
|
+
self._rec_handler = RecordHandler(self.user)
|
|
134
178
|
|
|
135
179
|
# Create empty caches to fill when necessary.
|
|
136
|
-
self.
|
|
137
|
-
self.
|
|
180
|
+
self._queried_all_steps = False
|
|
181
|
+
self._steps = {}
|
|
182
|
+
self._steps_by_id = {}
|
|
183
|
+
self._step_options = {}
|
|
184
|
+
self._step_updates = {}
|
|
185
|
+
|
|
186
|
+
self._tabs = []
|
|
187
|
+
self._tabs_by_name = {}
|
|
188
|
+
|
|
189
|
+
self._queried_all_tabs = False
|
|
190
|
+
|
|
138
191
|
# CR-46330: Cache any experiment entry information that might already exist in the context.
|
|
139
|
-
self.__queried_all_steps = False
|
|
140
192
|
# We can only trust the entries in the context if the experiment that this handler is for is the same as the
|
|
141
193
|
# one from the context.
|
|
142
194
|
if self.context is not None and self.context.eln_experiment == experiment:
|
|
195
|
+
cache_steps: list[ElnEntryStep] = []
|
|
143
196
|
if self.context.experiment_entry is not None:
|
|
144
|
-
|
|
197
|
+
cache_steps.append(ElnEntryStep(self._protocol, self.context.experiment_entry))
|
|
145
198
|
if self.context.experiment_entry_list is not None:
|
|
146
199
|
for entry in self.context.experiment_entry_list:
|
|
147
|
-
|
|
200
|
+
cache_steps.append(ElnEntryStep(self._protocol, entry))
|
|
201
|
+
for step in cache_steps:
|
|
202
|
+
self._steps.update({step.get_name(): step})
|
|
203
|
+
self._steps_by_id.update({step.get_id(): step})
|
|
148
204
|
|
|
149
205
|
@staticmethod
|
|
150
206
|
def __parse_params(context: UserIdentifier, experiment: ExperimentIdentifier | SapioRecord | None = None) \
|
|
@@ -181,10 +237,90 @@ class ExperimentHandler:
|
|
|
181
237
|
if not experiment:
|
|
182
238
|
raise SapioException(f"No experiment with record ID {record_id} located in the system.")
|
|
183
239
|
if experiment is None:
|
|
184
|
-
raise SapioException("Cannot initialize ExperimentHandler. No ELN Experiment found in the provided
|
|
240
|
+
raise SapioException("Cannot initialize ExperimentHandler. No ELN Experiment found in the provided "
|
|
241
|
+
"parameters.")
|
|
185
242
|
|
|
186
243
|
return user, context, experiment
|
|
187
244
|
|
|
245
|
+
@property
|
|
246
|
+
def protocol(self) -> ElnExperimentProtocol:
|
|
247
|
+
"""
|
|
248
|
+
The ELN experiment that this handler is for as a protocol object.
|
|
249
|
+
"""
|
|
250
|
+
return self._protocol
|
|
251
|
+
|
|
252
|
+
# CR-47485: Add methods for clearing and updating the caches of this ExperimentHandler.
|
|
253
|
+
def clear_all_caches(self) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Clear all caches that this ExperimentHandler uses.
|
|
256
|
+
"""
|
|
257
|
+
self.clear_step_caches()
|
|
258
|
+
self.clear_experiment_caches()
|
|
259
|
+
self.clear_tab_caches()
|
|
260
|
+
|
|
261
|
+
def clear_step_caches(self) -> None:
|
|
262
|
+
"""
|
|
263
|
+
Clear the step caches that this ExperimentHandler uses.
|
|
264
|
+
"""
|
|
265
|
+
self._queried_all_steps = False
|
|
266
|
+
self._steps.clear()
|
|
267
|
+
self._steps_by_id.clear()
|
|
268
|
+
self._step_options.clear()
|
|
269
|
+
self._step_updates.clear()
|
|
270
|
+
|
|
271
|
+
def clear_experiment_caches(self) -> None:
|
|
272
|
+
"""
|
|
273
|
+
Clear the experiment information caches that this ExperimentHandler uses.
|
|
274
|
+
"""
|
|
275
|
+
self._exp_record = None
|
|
276
|
+
self._exp_template = None
|
|
277
|
+
self._exp_options = {}
|
|
278
|
+
|
|
279
|
+
def clear_tab_caches(self) -> None:
|
|
280
|
+
"""
|
|
281
|
+
Clear the tab caches that this ExperimentHandler uses.
|
|
282
|
+
"""
|
|
283
|
+
self._queried_all_tabs = False
|
|
284
|
+
self._tabs.clear()
|
|
285
|
+
self._tabs_by_name.clear()
|
|
286
|
+
|
|
287
|
+
def add_entry_to_caches(self, entry: ExperimentEntry | ElnEntryStep) -> None:
|
|
288
|
+
"""
|
|
289
|
+
Add the given entry to the cache of steps for this experiment. This is necessary in order for certain methods to
|
|
290
|
+
work. You should only need to do this if you have created a new entry in your code using a method outside
|
|
291
|
+
of this ExperimentHandler.
|
|
292
|
+
|
|
293
|
+
:param entry: The entry to add to the cache.
|
|
294
|
+
"""
|
|
295
|
+
if isinstance(entry, ExperimentEntry):
|
|
296
|
+
entry = ElnEntryStep(self._protocol, entry)
|
|
297
|
+
self._steps.update({entry.get_name(): entry})
|
|
298
|
+
self._steps_by_id.update({entry.get_id(): entry})
|
|
299
|
+
# Skipping the options cache. The get_step_options method will update the cache when necessary.
|
|
300
|
+
|
|
301
|
+
def add_entries_to_caches(self, entries: list[ExperimentEntry | ElnEntryStep]) -> None:
|
|
302
|
+
"""
|
|
303
|
+
Add the given entries to the cache of steps for this experiment. This is necessary in order for certain methods
|
|
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
|
+
of this ExperimentHandler.
|
|
306
|
+
|
|
307
|
+
:param entries: The entries to add to the cache.
|
|
308
|
+
"""
|
|
309
|
+
for entry in entries:
|
|
310
|
+
self.add_entry_to_caches(entry)
|
|
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_name[tab.tab_name] = tab
|
|
323
|
+
|
|
188
324
|
# FR-46495: Split the creation of the experiment in launch_experiment into a create_experiment function.
|
|
189
325
|
@staticmethod
|
|
190
326
|
def create_experiment(context: SapioWebhookContext,
|
|
@@ -240,7 +376,8 @@ class ExperimentHandler:
|
|
|
240
376
|
template_name: str,
|
|
241
377
|
experiment_name: str | None = None,
|
|
242
378
|
parent_record: SapioRecord | None = None, *,
|
|
243
|
-
template_version: int | None = None,
|
|
379
|
+
template_version: int | None = None,
|
|
380
|
+
active_templates_only: bool = True) -> SapioWebhookResult:
|
|
244
381
|
"""
|
|
245
382
|
Create a SapioWebhookResult that, when returned by a webhook handler, sends the user to a new experiment of the
|
|
246
383
|
input template name.
|
|
@@ -276,24 +413,24 @@ class ExperimentHandler:
|
|
|
276
413
|
when the experiment template doesn't exist.
|
|
277
414
|
:return: This experiment's template. None if it has no template.
|
|
278
415
|
"""
|
|
279
|
-
template_id: int | None = self.
|
|
416
|
+
template_id: int | None = self._eln_exp.template_id
|
|
280
417
|
if template_id is None:
|
|
281
|
-
self.
|
|
418
|
+
self._exp_template = None
|
|
282
419
|
if exception_on_none:
|
|
283
|
-
raise SapioException(f"Experiment with ID {self.
|
|
420
|
+
raise SapioException(f"Experiment with ID {self._exp_id} has no template ID.")
|
|
284
421
|
return None
|
|
285
422
|
|
|
286
|
-
if not hasattr(self, "
|
|
423
|
+
if not hasattr(self, "_exp_template"):
|
|
287
424
|
# PR-46504: Allow inactive and non-latest version templates to be queried.
|
|
288
425
|
query = TemplateExperimentQueryPojo(template_id_white_list=[template_id],
|
|
289
426
|
active_templates_only=False,
|
|
290
427
|
latest_version_only=False)
|
|
291
|
-
templates: list[ElnTemplate] = self.
|
|
428
|
+
templates: list[ElnTemplate] = self._eln_man.get_template_experiment_list(query)
|
|
292
429
|
# PR-46504: Set the exp_template to None if there are no results.
|
|
293
|
-
self.
|
|
294
|
-
if self.
|
|
295
|
-
raise SapioException(f"Experiment template not found for experiment with ID {self.
|
|
296
|
-
return self.
|
|
430
|
+
self._exp_template = templates[0] if templates else None
|
|
431
|
+
if self._exp_template is None and exception_on_none:
|
|
432
|
+
raise SapioException(f"Experiment template not found for experiment with ID {self._exp_id}.")
|
|
433
|
+
return self._exp_template
|
|
297
434
|
|
|
298
435
|
# CR-46104: Change get_template_name to behave like NotebookProtocolImpl.getTemplateName (i.e. first see if the
|
|
299
436
|
# experiment template exists, and if not, see if the experiment record exists, instead of only checking the
|
|
@@ -310,18 +447,18 @@ class ExperimentHandler:
|
|
|
310
447
|
when the template name doesn't exist.
|
|
311
448
|
:return: The template name of the current experiment. None if it has no template name.
|
|
312
449
|
"""
|
|
313
|
-
if not hasattr(self, "
|
|
450
|
+
if not hasattr(self, "_exp_template"):
|
|
314
451
|
self.get_experiment_template(False)
|
|
315
|
-
if self.
|
|
452
|
+
if self._exp_template is None and not hasattr(self, "_exp_record"):
|
|
316
453
|
self.get_experiment_record(False)
|
|
317
454
|
|
|
318
455
|
name: str | None = None
|
|
319
|
-
if self.
|
|
320
|
-
name = self.
|
|
321
|
-
elif self.
|
|
322
|
-
name = self.
|
|
456
|
+
if self._exp_template is not None:
|
|
457
|
+
name = self._exp_template.template_name
|
|
458
|
+
elif self._exp_record is not None:
|
|
459
|
+
name = self._exp_record.get_field_value("TemplateExperimentName")
|
|
323
460
|
if name is None and exception_on_none:
|
|
324
|
-
raise SapioException(f"Template name not found for experiment with ID {self.
|
|
461
|
+
raise SapioException(f"Template name not found for experiment with ID {self._exp_id}.")
|
|
325
462
|
return name
|
|
326
463
|
|
|
327
464
|
def get_experiment_record(self, exception_on_none: bool = True) -> DataRecord | None:
|
|
@@ -332,21 +469,23 @@ class ExperimentHandler:
|
|
|
332
469
|
when the experiment record doesn't exist.
|
|
333
470
|
:return: The data record for this experiment. None if it has no record.
|
|
334
471
|
"""
|
|
335
|
-
if not hasattr(self, "
|
|
336
|
-
self.
|
|
337
|
-
if self.
|
|
338
|
-
raise SapioException(f"Experiment record not found for experiment with ID {self.
|
|
339
|
-
return self.
|
|
472
|
+
if not hasattr(self, "_exp_record"):
|
|
473
|
+
self._exp_record = self._protocol.get_record()
|
|
474
|
+
if self._exp_record is None and exception_on_none:
|
|
475
|
+
raise SapioException(f"Experiment record not found for experiment with ID {self._exp_id}.")
|
|
476
|
+
return self._exp_record
|
|
340
477
|
|
|
341
|
-
|
|
478
|
+
# CR-47491: Support not providing a wrapper type to receive PyRecordModels instead of WrappedRecordModels.
|
|
479
|
+
def get_experiment_model(self, wrapper_type: type[WrappedType] | None = None) -> WrappedType | PyRecordModel:
|
|
342
480
|
"""
|
|
343
481
|
Query for the data record of this experiment and wrap it as a record model with the given wrapper.
|
|
344
482
|
The returned record is cached by the ExperimentHandler.
|
|
345
483
|
|
|
346
|
-
:param wrapper_type: The record model wrapper to use.
|
|
484
|
+
:param wrapper_type: The record model wrapper to use. If not provided, the record is returned as a
|
|
485
|
+
PyRecordModel instead of a WrappedRecordModel.
|
|
347
486
|
:return: The record model for this experiment.
|
|
348
487
|
"""
|
|
349
|
-
return self.
|
|
488
|
+
return self._rec_handler.wrap_model(self.get_experiment_record(), wrapper_type)
|
|
350
489
|
|
|
351
490
|
def update_experiment(self,
|
|
352
491
|
experiment_name: str | None = None,
|
|
@@ -368,14 +507,14 @@ class ExperimentHandler:
|
|
|
368
507
|
criteria.new_experiment_name = experiment_name
|
|
369
508
|
criteria.new_experiment_status = experiment_status
|
|
370
509
|
criteria.experiment_option_map = experiment_option_map
|
|
371
|
-
self.
|
|
510
|
+
self._eln_man.update_notebook_experiment(self._exp_id, criteria)
|
|
372
511
|
|
|
373
512
|
if experiment_name is not None:
|
|
374
|
-
self.
|
|
513
|
+
self._eln_exp.notebook_experiment_name = experiment_name
|
|
375
514
|
if experiment_status is not None:
|
|
376
|
-
self.
|
|
515
|
+
self._eln_exp.notebook_experiment_status = experiment_status
|
|
377
516
|
if experiment_option_map is not None:
|
|
378
|
-
self.
|
|
517
|
+
self._exp_options = experiment_option_map
|
|
379
518
|
|
|
380
519
|
def get_experiment_option(self, option: str) -> str:
|
|
381
520
|
"""
|
|
@@ -400,7 +539,10 @@ class ExperimentHandler:
|
|
|
400
539
|
|
|
401
540
|
:return: The map of options for this experiment.
|
|
402
541
|
"""
|
|
403
|
-
|
|
542
|
+
if hasattr(self, "_exp_options"):
|
|
543
|
+
return self._exp_options
|
|
544
|
+
self._exp_options = self._eln_man.get_notebook_experiment_options(self._exp_id)
|
|
545
|
+
return self._exp_options
|
|
404
546
|
|
|
405
547
|
def add_experiment_options(self, mapping: Mapping[str, str]) -> None:
|
|
406
548
|
"""
|
|
@@ -425,7 +567,7 @@ class ExperimentHandler:
|
|
|
425
567
|
|
|
426
568
|
:return: True if the experiment's status is Completed or CompletedApproved. False otherwise.
|
|
427
569
|
"""
|
|
428
|
-
return self.
|
|
570
|
+
return self._eln_exp.notebook_experiment_status in self._EXPERIMENT_COMPLETE_STATUSES
|
|
429
571
|
|
|
430
572
|
def experiment_is_canceled(self) -> bool:
|
|
431
573
|
"""
|
|
@@ -433,7 +575,7 @@ class ExperimentHandler:
|
|
|
433
575
|
|
|
434
576
|
:return: True if the experiment's status is Canceled. False otherwise.
|
|
435
577
|
"""
|
|
436
|
-
return self.
|
|
578
|
+
return self._eln_exp.notebook_experiment_status == ElnExperimentStatus.Canceled
|
|
437
579
|
|
|
438
580
|
def experiment_is_locked(self) -> bool:
|
|
439
581
|
"""
|
|
@@ -442,7 +584,7 @@ class ExperimentHandler:
|
|
|
442
584
|
:return: True if the experiment's status is Completed, CompletedApproved, Canceled, LockedAwaitingApproval,
|
|
443
585
|
or LockedRejected. False otherwise.
|
|
444
586
|
"""
|
|
445
|
-
return self.
|
|
587
|
+
return self._eln_exp.notebook_experiment_status in self._EXPERIMENT_LOCKED_STATUSES
|
|
446
588
|
|
|
447
589
|
def complete_experiment(self) -> None:
|
|
448
590
|
"""
|
|
@@ -450,8 +592,8 @@ class ExperimentHandler:
|
|
|
450
592
|
experiment is already completed, and does nothing if so.
|
|
451
593
|
"""
|
|
452
594
|
if not self.experiment_is_complete():
|
|
453
|
-
self.
|
|
454
|
-
self.
|
|
595
|
+
self._protocol.complete_protocol()
|
|
596
|
+
self._eln_exp.notebook_experiment_status = ElnExperimentStatus.Completed
|
|
455
597
|
|
|
456
598
|
def cancel_experiment(self) -> None:
|
|
457
599
|
"""
|
|
@@ -463,8 +605,8 @@ class ExperimentHandler:
|
|
|
463
605
|
as those changes are tied to the button instead of being on the experiment status change.
|
|
464
606
|
"""
|
|
465
607
|
if not self.experiment_is_canceled():
|
|
466
|
-
self.
|
|
467
|
-
self.
|
|
608
|
+
self._protocol.cancel_protocol()
|
|
609
|
+
self._eln_exp.notebook_experiment_status = ElnExperimentStatus.Canceled
|
|
468
610
|
|
|
469
611
|
def step_exists(self, step_name: str) -> bool:
|
|
470
612
|
"""
|
|
@@ -523,13 +665,13 @@ class ExperimentHandler:
|
|
|
523
665
|
# If we haven't queried the system for all steps in the experiment yet, then the reason that a step is
|
|
524
666
|
# missing may be because it wasn't in the webhook context. Therefore, query all steps and then check
|
|
525
667
|
# if the step name is still missing from the experiment before potentially throwing an exception.
|
|
526
|
-
if self.
|
|
527
|
-
self.
|
|
528
|
-
self.
|
|
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()})
|
|
529
671
|
|
|
530
|
-
step: ElnEntryStep = self.
|
|
672
|
+
step: ElnEntryStep = self._steps.get(name)
|
|
531
673
|
if step is None and exception_on_none is True:
|
|
532
|
-
raise SapioException(f"ElnEntryStep of name \"{name}\" not found in experiment with ID {self.
|
|
674
|
+
raise SapioException(f"ElnEntryStep of name \"{name}\" not found in experiment with ID {self._exp_id}.")
|
|
533
675
|
ret_list.append(step)
|
|
534
676
|
return ret_list
|
|
535
677
|
|
|
@@ -543,10 +685,10 @@ class ExperimentHandler:
|
|
|
543
685
|
a data type name or wrapper is given, only returns entries that match that data type name or wrapper.
|
|
544
686
|
:return: Every entry in the experiment in order of appearance that match the provided data type, if any.
|
|
545
687
|
"""
|
|
546
|
-
if self.
|
|
547
|
-
self.
|
|
548
|
-
self.
|
|
549
|
-
all_steps: list[ElnEntryStep] = self.
|
|
688
|
+
if self._queried_all_steps is False:
|
|
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()
|
|
550
692
|
if data_type is None:
|
|
551
693
|
return all_steps
|
|
552
694
|
data_type: str = AliasUtil.to_data_type_name(data_type)
|
|
@@ -600,7 +742,8 @@ class ExperimentHandler:
|
|
|
600
742
|
"""
|
|
601
743
|
return self.__to_eln_step(step).get_records()
|
|
602
744
|
|
|
603
|
-
def get_step_models(self, step: Step, wrapper_type: type[WrappedType])
|
|
745
|
+
def get_step_models(self, step: Step, wrapper_type: type[WrappedType] | None = None) \
|
|
746
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
604
747
|
"""
|
|
605
748
|
Query for the data records for the given step and wrap them as record models with the given type. The returned
|
|
606
749
|
records are not cached by the ExperimentHandler.
|
|
@@ -612,10 +755,11 @@ class ExperimentHandler:
|
|
|
612
755
|
The step to get the data records for.
|
|
613
756
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
614
757
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
615
|
-
:param wrapper_type: The record model wrapper to use.
|
|
758
|
+
:param wrapper_type: The record model wrapper to use. If not provided, the records are returned as
|
|
759
|
+
PyRecordModels instead of WrappedRecordModels.
|
|
616
760
|
:return: The record models for the given step.
|
|
617
761
|
"""
|
|
618
|
-
return self.
|
|
762
|
+
return self._rec_handler.wrap_models(self.get_step_records(step), wrapper_type)
|
|
619
763
|
|
|
620
764
|
def add_step_records(self, step: Step, records: Iterable[SapioRecord]) -> None:
|
|
621
765
|
"""
|
|
@@ -719,10 +863,13 @@ class ExperimentHandler:
|
|
|
719
863
|
The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
|
|
720
864
|
"""
|
|
721
865
|
self.set_step_records(step, [record])
|
|
866
|
+
step = self.__to_eln_step(step)
|
|
867
|
+
if isinstance(step.eln_entry, ExperimentFormEntry):
|
|
868
|
+
step.eln_entry.record_id = AliasUtil.to_data_record(record).record_id
|
|
722
869
|
|
|
723
870
|
# FR-46496 - Provide functions for adding and removing rows from an ELN data type entry.
|
|
724
871
|
def add_eln_rows(self, step: Step, count: int, wrapper_type: type[WrappedType] | None = None) \
|
|
725
|
-
-> list[
|
|
872
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
726
873
|
"""
|
|
727
874
|
Add rows to an ELNExperimentDetail or ELNSampleDetail table entry. The rows will not appear in the system
|
|
728
875
|
until a record manager store and commit has occurred.
|
|
@@ -744,12 +891,12 @@ class ExperimentHandler:
|
|
|
744
891
|
dt: str = step.get_data_type_names()[0]
|
|
745
892
|
if not ElnBaseDataType.is_eln_type(dt):
|
|
746
893
|
raise SapioException("The provided step is not an ELN data type entry.")
|
|
747
|
-
records: list[PyRecordModel] = self.
|
|
894
|
+
records: list[PyRecordModel] = self._inst_man.add_new_records(dt, count)
|
|
748
895
|
if wrapper_type:
|
|
749
|
-
return self.
|
|
896
|
+
return self._inst_man.wrap_list(records, wrapper_type)
|
|
750
897
|
return records
|
|
751
898
|
|
|
752
|
-
def add_eln_row(self, step: Step, wrapper_type: type[WrappedType] | None = None) ->
|
|
899
|
+
def add_eln_row(self, step: Step, wrapper_type: type[WrappedType] | None = None) -> WrappedType | PyRecordModel:
|
|
753
900
|
"""
|
|
754
901
|
Add a row to an ELNExperimentDetail or ELNSampleDetail table entry. The row will not appear in the system
|
|
755
902
|
until a record manager store and commit has occurred.
|
|
@@ -803,7 +950,7 @@ class ExperimentHandler:
|
|
|
803
950
|
else:
|
|
804
951
|
record.delete()
|
|
805
952
|
if data_records:
|
|
806
|
-
record_models: list[PyRecordModel] = self.
|
|
953
|
+
record_models: list[PyRecordModel] = self._inst_man.add_existing_records(data_records)
|
|
807
954
|
for record in record_models:
|
|
808
955
|
record.delete()
|
|
809
956
|
|
|
@@ -827,8 +974,9 @@ class ExperimentHandler:
|
|
|
827
974
|
"""
|
|
828
975
|
self.remove_eln_rows(step, [record])
|
|
829
976
|
|
|
830
|
-
def add_sample_details(self, step: Step, samples: list[RecordModel],
|
|
831
|
-
|
|
977
|
+
def add_sample_details(self, step: Step, samples: list[RecordModel],
|
|
978
|
+
wrapper_type: type[WrappedType] | None = None) \
|
|
979
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
832
980
|
"""
|
|
833
981
|
Add sample details to a sample details entry while relating them to the input sample records.
|
|
834
982
|
|
|
@@ -858,9 +1006,10 @@ class ExperimentHandler:
|
|
|
858
1006
|
})
|
|
859
1007
|
records.append(detail)
|
|
860
1008
|
if wrapper_type:
|
|
861
|
-
return self.
|
|
1009
|
+
return self._inst_man.wrap_list(records, wrapper_type)
|
|
862
1010
|
return records
|
|
863
1011
|
|
|
1012
|
+
# noinspection PyPep8Naming
|
|
864
1013
|
def update_step(self, step: Step,
|
|
865
1014
|
entry_name: str | None = None,
|
|
866
1015
|
related_entry_set: Iterable[int] | None = None,
|
|
@@ -927,8 +1076,227 @@ class ExperimentHandler:
|
|
|
927
1076
|
If you wish to add options to the existing map of options that an entry has, use the
|
|
928
1077
|
add_step_options method.
|
|
929
1078
|
"""
|
|
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.",
|
|
1082
|
+
DeprecationWarning)
|
|
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)
|
|
1106
|
+
|
|
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.
|
|
1134
|
+
|
|
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.
|
|
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)
|
|
1199
|
+
self.force_step_update(step, update)
|
|
1200
|
+
|
|
1201
|
+
def force_step_update(self, step: Step, update: AbstractElnEntryUpdateCriteria) -> None:
|
|
1202
|
+
"""
|
|
1203
|
+
Immediately sent an update to an entry in this experiment. All changes will be reflected by the ExperimentEntry
|
|
1204
|
+
of the Step that is being updated.
|
|
1205
|
+
|
|
1206
|
+
Consider using store_step_update and commit_step_updates instead if the update does not need to be immediate.
|
|
1207
|
+
|
|
1208
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1209
|
+
list of steps in the experiment and caches them.
|
|
1210
|
+
|
|
1211
|
+
:param step: The step to update.
|
|
1212
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1213
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1214
|
+
:param update: The update to make to the step.
|
|
1215
|
+
"""
|
|
1216
|
+
step = self.__to_eln_step(step)
|
|
1217
|
+
self._eln_man.update_experiment_entry(self._exp_id, step.get_id(), update)
|
|
1218
|
+
self._update_entry_details(step, update)
|
|
1219
|
+
|
|
1220
|
+
def store_step_update(self, step: Step, update: AbstractElnEntryUpdateCriteria) -> None:
|
|
1221
|
+
"""
|
|
1222
|
+
Store updates to be made to an entry in this experiment. The updates are not committed until
|
|
1223
|
+
commit_entry_updates is called.
|
|
1224
|
+
|
|
1225
|
+
If the same entry is updated multiple times before committing, the latest update will be merged on top of the
|
|
1226
|
+
previous updates; where the new update and old update conflict, the new update will take precedence.
|
|
1227
|
+
|
|
1228
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1229
|
+
list of steps in the experiment and caches them.
|
|
1230
|
+
|
|
1231
|
+
:param step: The step to update.
|
|
1232
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1233
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1234
|
+
:param update: The update to make to the step.
|
|
1235
|
+
"""
|
|
1236
|
+
step = self.__to_eln_step(step)
|
|
1237
|
+
if step.eln_entry.entry_type != update.entry_type:
|
|
1238
|
+
raise SapioException(f"The provided step and update criteria are not of the same entry type. "
|
|
1239
|
+
f"The step is of type {step.eln_entry.entry_type} and the update criteria is of type "
|
|
1240
|
+
f"{update.entry_type}.")
|
|
1241
|
+
if step.get_id() in self._step_updates:
|
|
1242
|
+
self._merge_updates(update, self._step_updates[step.get_id()])
|
|
1243
|
+
else:
|
|
1244
|
+
self._step_updates[step.get_id()] = update
|
|
1245
|
+
|
|
1246
|
+
def store_step_updates(self, updates: dict[Step, AbstractElnEntryUpdateCriteria]) -> None:
|
|
1247
|
+
"""
|
|
1248
|
+
Store updates to be made to multiple entries in this experiment. The updates are not committed until
|
|
1249
|
+
commit_entry_updates is called.
|
|
1250
|
+
|
|
1251
|
+
If the same entry is updated multiple times before committing, the latest update will be merged on top of the
|
|
1252
|
+
previous updates; where the new update and old update conflict, the new update will take precedence.
|
|
1253
|
+
|
|
1254
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1255
|
+
list of steps in the experiment and caches them.
|
|
1256
|
+
|
|
1257
|
+
:param updates: A dictionary of steps and their respective updates.
|
|
1258
|
+
"""
|
|
1259
|
+
for step, update in updates.items():
|
|
1260
|
+
self.store_step_update(step, update)
|
|
1261
|
+
|
|
1262
|
+
def commit_step_updates(self) -> None:
|
|
1263
|
+
"""
|
|
1264
|
+
Commit all the stored updates to the entries in this experiment. The updates are made in the order that they
|
|
1265
|
+
were stored.
|
|
1266
|
+
"""
|
|
1267
|
+
self._eln_man.update_experiment_entries(self._exp_id, self._step_updates)
|
|
1268
|
+
for step_id, criteria in self._step_updates.items():
|
|
1269
|
+
self._update_entry_details(self._steps_by_id[step_id], criteria)
|
|
1270
|
+
self._step_updates.clear()
|
|
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
|
+
"""
|
|
930
1298
|
step: ElnEntryStep = self.__to_eln_step(step)
|
|
931
|
-
|
|
1299
|
+
update = AbstractElnEntryUpdateCriteria(step.eln_entry.entry_type)
|
|
932
1300
|
|
|
933
1301
|
# These two variables could be iterables that aren't lists. Convert them to plain
|
|
934
1302
|
# lists, since that's what the update criteria is expecting.
|
|
@@ -937,81 +1305,154 @@ class ExperimentHandler:
|
|
|
937
1305
|
if dependency_set is not None:
|
|
938
1306
|
dependency_set = list(dependency_set)
|
|
939
1307
|
|
|
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
|
-
|
|
965
|
-
|
|
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
|
+
|
|
1333
|
+
@staticmethod
|
|
1334
|
+
def _merge_updates(new_update: AbstractElnEntryUpdateCriteria, old_update: AbstractElnEntryUpdateCriteria) -> None:
|
|
1335
|
+
"""
|
|
1336
|
+
Merge the new update criteria onto the old update criteria. The new update will take precedence where there
|
|
1337
|
+
are conflicts.
|
|
1338
|
+
"""
|
|
1339
|
+
for key, value in new_update.__dict__.items():
|
|
1340
|
+
if value is not None:
|
|
1341
|
+
old_update.__dict__[key] = value
|
|
1342
|
+
|
|
1343
|
+
def _update_entry_details(self, step: Step, update: AbstractElnEntryUpdateCriteria) -> None:
|
|
1344
|
+
"""
|
|
1345
|
+
Update the cached information for this entry in case it's needed by the caller after updating.
|
|
1346
|
+
"""
|
|
966
1347
|
entry: ExperimentEntry = step.eln_entry
|
|
967
|
-
if entry_name is not None:
|
|
1348
|
+
if update.entry_name is not None:
|
|
968
1349
|
# PR-46477 - Ensure that the previous name of the updated entry already existed in the cache.
|
|
969
|
-
if entry.entry_name in self.
|
|
970
|
-
self.
|
|
971
|
-
entry.entry_name = entry_name
|
|
972
|
-
self.
|
|
973
|
-
if related_entry_set is not None:
|
|
974
|
-
entry.related_entry_id_set = related_entry_set
|
|
975
|
-
if dependency_set is not None:
|
|
976
|
-
entry.dependency_set = dependency_set
|
|
977
|
-
if entry_status is not None:
|
|
978
|
-
entry.entry_status = entry_status
|
|
979
|
-
if order is not None:
|
|
980
|
-
entry.order = order
|
|
981
|
-
if description is not None:
|
|
982
|
-
entry.description = description
|
|
983
|
-
if requires_grabber_plugin is not None:
|
|
984
|
-
entry.requires_grabber_plugin = requires_grabber_plugin
|
|
985
|
-
if is_initialization_required is not None:
|
|
986
|
-
entry.is_initialization_required = is_initialization_required
|
|
987
|
-
if notebook_experiment_tab_id is not None:
|
|
988
|
-
entry.notebook_experiment_tab_id = notebook_experiment_tab_id
|
|
989
|
-
if entry_height is not None:
|
|
990
|
-
entry.entry_height = entry_height
|
|
991
|
-
if column_order is not None:
|
|
992
|
-
entry.column_order = column_order
|
|
993
|
-
if column_span is not None:
|
|
994
|
-
entry.column_span = column_span
|
|
995
|
-
if is_removable is not None:
|
|
996
|
-
entry.is_removable = is_removable
|
|
997
|
-
if is_renamable is not None:
|
|
998
|
-
entry.is_renamable = is_renamable
|
|
999
|
-
if source_entry_id is not None:
|
|
1000
|
-
entry.source_entry_id = source_entry_id
|
|
1001
|
-
if clear_source_entry_id is True:
|
|
1350
|
+
if entry.entry_name in self._steps:
|
|
1351
|
+
self._steps.pop(entry.entry_name)
|
|
1352
|
+
entry.entry_name = update.entry_name
|
|
1353
|
+
self._steps.update({update.entry_name: step})
|
|
1354
|
+
if update.related_entry_set is not None:
|
|
1355
|
+
entry.related_entry_id_set = update.related_entry_set
|
|
1356
|
+
if update.dependency_set is not None:
|
|
1357
|
+
entry.dependency_set = update.dependency_set
|
|
1358
|
+
if update.entry_status is not None:
|
|
1359
|
+
entry.entry_status = update.entry_status
|
|
1360
|
+
if update.order is not None:
|
|
1361
|
+
entry.order = update.order
|
|
1362
|
+
if update.description is not None:
|
|
1363
|
+
entry.description = update.description
|
|
1364
|
+
if update.requires_grabber_plugin is not None:
|
|
1365
|
+
entry.requires_grabber_plugin = update.requires_grabber_plugin
|
|
1366
|
+
if update.is_initialization_required is not None:
|
|
1367
|
+
entry.is_initialization_required = update.is_initialization_required
|
|
1368
|
+
if update.notebook_experiment_tab_id is not None:
|
|
1369
|
+
entry.notebook_experiment_tab_id = update.notebook_experiment_tab_id
|
|
1370
|
+
if update.entry_height is not None:
|
|
1371
|
+
entry.entry_height = update.entry_height
|
|
1372
|
+
if update.column_order is not None:
|
|
1373
|
+
entry.column_order = update.column_order
|
|
1374
|
+
if update.column_span is not None:
|
|
1375
|
+
entry.column_span = update.column_span
|
|
1376
|
+
if update.is_removable is not None:
|
|
1377
|
+
entry.is_removable = update.is_removable
|
|
1378
|
+
if update.is_renamable is not None:
|
|
1379
|
+
entry.is_renamable = update.is_renamable
|
|
1380
|
+
if update.source_entry_id is not None:
|
|
1381
|
+
entry.source_entry_id = update.source_entry_id
|
|
1382
|
+
if update.clear_source_entry_id is True:
|
|
1002
1383
|
entry.source_entry_id = None
|
|
1003
|
-
if is_hidden is not None:
|
|
1004
|
-
entry.is_hidden = is_hidden
|
|
1005
|
-
if is_static_View is not None:
|
|
1006
|
-
entry.is_static_View = is_static_View
|
|
1007
|
-
if is_shown_in_template is not None:
|
|
1008
|
-
entry.is_shown_in_template = is_shown_in_template
|
|
1009
|
-
if template_item_fulfilled_timestamp is not None:
|
|
1010
|
-
entry.template_item_fulfilled_timestamp = template_item_fulfilled_timestamp
|
|
1011
|
-
if clear_template_item_fulfilled_timestamp is True:
|
|
1384
|
+
if update.is_hidden is not None:
|
|
1385
|
+
entry.is_hidden = update.is_hidden
|
|
1386
|
+
if update.is_static_View is not None:
|
|
1387
|
+
entry.is_static_View = update.is_static_View
|
|
1388
|
+
if update.is_shown_in_template is not None:
|
|
1389
|
+
entry.is_shown_in_template = update.is_shown_in_template
|
|
1390
|
+
if update.template_item_fulfilled_timestamp is not None:
|
|
1391
|
+
entry.template_item_fulfilled_timestamp = update.template_item_fulfilled_timestamp
|
|
1392
|
+
if update.clear_template_item_fulfilled_timestamp is True:
|
|
1012
1393
|
entry.template_item_fulfilled_timestamp = None
|
|
1013
|
-
if entry_options_map is not None:
|
|
1014
|
-
self.
|
|
1394
|
+
if update.entry_options_map is not None:
|
|
1395
|
+
self._step_options.update({step.get_id(): update.entry_options_map})
|
|
1396
|
+
|
|
1397
|
+
if isinstance(entry, ExperimentAttachmentEntry) and isinstance(update, ElnAttachmentEntryUpdateCriteria):
|
|
1398
|
+
if update.entry_attachment_list is not None:
|
|
1399
|
+
entry.entry_attachment_list = update.entry_attachment_list
|
|
1400
|
+
if update.record_id is not None:
|
|
1401
|
+
entry.record_id = update.record_id
|
|
1402
|
+
if update.attachment_name is not None:
|
|
1403
|
+
entry.attachment_name = update.attachment_name
|
|
1404
|
+
elif isinstance(entry, ExperimentDashboardEntry) and isinstance(update, ElnDashboardEntryUpdateCriteria):
|
|
1405
|
+
if update.dashboard_guid is not None:
|
|
1406
|
+
entry.dashboard_guid = update.dashboard_guid
|
|
1407
|
+
if update.dashboard_guid_list is not None:
|
|
1408
|
+
entry.dashboard_guid_list = update.dashboard_guid_list
|
|
1409
|
+
if update.data_source_entry_id is not None:
|
|
1410
|
+
entry.data_source_entry_id = update.data_source_entry_id
|
|
1411
|
+
elif isinstance(entry, ExperimentFormEntry) and isinstance(update, ElnFormEntryUpdateCriteria):
|
|
1412
|
+
if update.record_id is not None:
|
|
1413
|
+
entry.record_id = update.record_id
|
|
1414
|
+
if update.form_name_list is not None:
|
|
1415
|
+
entry.form_name_list = update.form_name_list
|
|
1416
|
+
if update.data_type_layout_name is not None:
|
|
1417
|
+
entry.data_type_layout_name = update.data_type_layout_name
|
|
1418
|
+
if update.field_set_id_list is not None:
|
|
1419
|
+
entry.field_set_id_list = update.field_set_id_list
|
|
1420
|
+
if update.extension_type_list is not None:
|
|
1421
|
+
entry.extension_type_list = update.extension_type_list
|
|
1422
|
+
if update.data_field_name_list is not None:
|
|
1423
|
+
entry.data_field_name_list = update.data_field_name_list
|
|
1424
|
+
if update.is_existing_field_removable is not None:
|
|
1425
|
+
entry.is_existing_field_removable = update.is_existing_field_removable
|
|
1426
|
+
if update.is_field_addable is not None:
|
|
1427
|
+
entry.is_field_addable = update.is_field_addable
|
|
1428
|
+
elif isinstance(entry, ExperimentPluginEntry) and isinstance(update, ElnPluginEntryUpdateCriteria):
|
|
1429
|
+
if update.plugin_name is not None:
|
|
1430
|
+
entry.plugin_name = update.plugin_name
|
|
1431
|
+
if update.provides_template_data is not None:
|
|
1432
|
+
entry.provides_template_data = update.provides_template_data
|
|
1433
|
+
if update.using_template_data is not None:
|
|
1434
|
+
entry.using_template_data = update.using_template_data
|
|
1435
|
+
if isinstance(entry, ExperimentTableEntry) and isinstance(update, ElnTableEntryUpdateCriteria):
|
|
1436
|
+
if update.data_type_layout_name is not None:
|
|
1437
|
+
entry.data_type_layout_name = update.data_type_layout_name
|
|
1438
|
+
if update.extension_type_list is not None:
|
|
1439
|
+
entry.extension_type_list = update.extension_type_list
|
|
1440
|
+
if update.field_set_id_list is not None:
|
|
1441
|
+
entry.field_set_id_list = update.field_set_id_list
|
|
1442
|
+
if update.is_existing_field_removable is not None:
|
|
1443
|
+
entry.is_existing_field_removable = update.is_existing_field_removable
|
|
1444
|
+
if update.is_field_addable is not None:
|
|
1445
|
+
entry.is_field_addable = update.is_field_addable
|
|
1446
|
+
if update.show_key_fields is not None:
|
|
1447
|
+
entry.show_key_fields = update.show_key_fields
|
|
1448
|
+
if update.table_column_list is not None:
|
|
1449
|
+
entry.table_column_list = update.table_column_list
|
|
1450
|
+
elif isinstance(entry, ExperimentTempDataEntry) and isinstance(update, ElnTempDataEntryUpdateCriteria):
|
|
1451
|
+
if update.plugin_path is not None:
|
|
1452
|
+
entry.plugin_path = update.plugin_path
|
|
1453
|
+
elif isinstance(entry, ExperimentTextEntry) and isinstance(update, ElnTextEntryUpdateCriteria):
|
|
1454
|
+
# Text update criteria has no additional fields.
|
|
1455
|
+
pass
|
|
1015
1456
|
|
|
1016
1457
|
def get_step_option(self, step: Step, option: str) -> str:
|
|
1017
1458
|
"""
|
|
@@ -1051,9 +1492,10 @@ class ExperimentHandler:
|
|
|
1051
1492
|
:return: The map of options for the input step.
|
|
1052
1493
|
"""
|
|
1053
1494
|
step = self.__to_eln_step(step)
|
|
1054
|
-
if step not in self.
|
|
1055
|
-
self.
|
|
1056
|
-
|
|
1495
|
+
if step not in self._step_options:
|
|
1496
|
+
self._step_options.update(ExperimentReportUtil.get_experiment_entry_options(self.user,
|
|
1497
|
+
self.get_all_steps()))
|
|
1498
|
+
return self._step_options[step.get_id()]
|
|
1057
1499
|
|
|
1058
1500
|
def add_step_options(self, step: Step, mapping: Mapping[str, str]):
|
|
1059
1501
|
"""
|
|
@@ -1077,7 +1519,7 @@ class ExperimentHandler:
|
|
|
1077
1519
|
"""
|
|
1078
1520
|
options: dict[str, str] = self.get_step_options(step)
|
|
1079
1521
|
options.update(mapping)
|
|
1080
|
-
self.
|
|
1522
|
+
self.force_step_update_params(step, entry_options_map=options)
|
|
1081
1523
|
|
|
1082
1524
|
def initialize_step(self, step: Step) -> None:
|
|
1083
1525
|
"""
|
|
@@ -1095,7 +1537,7 @@ class ExperimentHandler:
|
|
|
1095
1537
|
# Avoid unnecessary calls if the step is already initialized.
|
|
1096
1538
|
step: ElnEntryStep = self.__to_eln_step(step)
|
|
1097
1539
|
if step.eln_entry.template_item_fulfilled_timestamp is None:
|
|
1098
|
-
self.
|
|
1540
|
+
self.force_step_update_params(step, template_item_fulfilled_timestamp=TimeUtil.now_in_millis())
|
|
1099
1541
|
|
|
1100
1542
|
def uninitialize_step(self, step: Step) -> None:
|
|
1101
1543
|
"""
|
|
@@ -1113,7 +1555,7 @@ class ExperimentHandler:
|
|
|
1113
1555
|
# Avoid unnecessary calls if the step is already uninitialized.
|
|
1114
1556
|
step: ElnEntryStep = self.__to_eln_step(step)
|
|
1115
1557
|
if step.eln_entry.template_item_fulfilled_timestamp is not None:
|
|
1116
|
-
self.
|
|
1558
|
+
self.force_step_update_params(step, clear_template_item_fulfilled_timestamp=True)
|
|
1117
1559
|
|
|
1118
1560
|
def complete_step(self, step: Step) -> None:
|
|
1119
1561
|
"""
|
|
@@ -1129,7 +1571,7 @@ class ExperimentHandler:
|
|
|
1129
1571
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1130
1572
|
"""
|
|
1131
1573
|
step = self.__to_eln_step(step)
|
|
1132
|
-
if step.eln_entry.entry_status not in self.
|
|
1574
|
+
if step.eln_entry.entry_status not in self._ENTRY_COMPLETE_STATUSES:
|
|
1133
1575
|
step.complete_step()
|
|
1134
1576
|
step.eln_entry.entry_status = ExperimentEntryStatus.Completed
|
|
1135
1577
|
|
|
@@ -1147,7 +1589,7 @@ class ExperimentHandler:
|
|
|
1147
1589
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1148
1590
|
"""
|
|
1149
1591
|
step = self.__to_eln_step(step)
|
|
1150
|
-
if step.eln_entry.entry_status in self.
|
|
1592
|
+
if step.eln_entry.entry_status in self._ENTRY_LOCKED_STATUSES:
|
|
1151
1593
|
step.unlock_step()
|
|
1152
1594
|
step.eln_entry.entry_status = ExperimentEntryStatus.UnlockedChangesRequired
|
|
1153
1595
|
|
|
@@ -1169,8 +1611,8 @@ class ExperimentHandler:
|
|
|
1169
1611
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1170
1612
|
"""
|
|
1171
1613
|
step = self.__to_eln_step(step)
|
|
1172
|
-
if step.eln_entry.entry_status in self.
|
|
1173
|
-
self.
|
|
1614
|
+
if step.eln_entry.entry_status in self._ENTRY_LOCKED_STATUSES:
|
|
1615
|
+
self.force_step_update_params(step, entry_status=ExperimentEntryStatus.Disabled)
|
|
1174
1616
|
|
|
1175
1617
|
def step_is_submitted(self, step: Step) -> bool:
|
|
1176
1618
|
"""
|
|
@@ -1185,7 +1627,7 @@ class ExperimentHandler:
|
|
|
1185
1627
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1186
1628
|
:return: True if the step's status is Completed or CompletedApproved. False otherwise.
|
|
1187
1629
|
"""
|
|
1188
|
-
return self.__to_eln_step(step).eln_entry.entry_status in self.
|
|
1630
|
+
return self.__to_eln_step(step).eln_entry.entry_status in self._ENTRY_COMPLETE_STATUSES
|
|
1189
1631
|
|
|
1190
1632
|
def step_is_locked(self, step: Step) -> bool:
|
|
1191
1633
|
"""
|
|
@@ -1201,7 +1643,494 @@ class ExperimentHandler:
|
|
|
1201
1643
|
:return: True if the step's status is Completed, CompletedApproved, Disabled, LockedAwaitingApproval,
|
|
1202
1644
|
or LockedRejected. False otherwise.
|
|
1203
1645
|
"""
|
|
1204
|
-
return self.__to_eln_step(step).eln_entry.entry_status in self.
|
|
1646
|
+
return self.__to_eln_step(step).eln_entry.entry_status in self._ENTRY_LOCKED_STATUSES
|
|
1647
|
+
|
|
1648
|
+
# FR-47464: Some functions that can help with entry placement.
|
|
1649
|
+
def get_all_tabs(self) -> list[ElnExperimentTab]:
|
|
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
|
+
:return: A list of all the tabs in the experiment in order of appearance.
|
|
1655
|
+
"""
|
|
1656
|
+
if not self._queried_all_tabs:
|
|
1657
|
+
self._tabs = self._eln_man.get_tabs_for_experiment(self._exp_id)
|
|
1658
|
+
self._tabs.sort(key=lambda t: t.tab_order)
|
|
1659
|
+
self._tabs_by_name = {tab.tab_name: tab for tab in self._tabs}
|
|
1660
|
+
return self._tabs
|
|
1661
|
+
|
|
1662
|
+
def get_first_tab(self) -> ElnExperimentTab:
|
|
1663
|
+
"""
|
|
1664
|
+
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1665
|
+
list of tabs in the experiment and caches them.
|
|
1666
|
+
|
|
1667
|
+
:return: The first tab in the experiment.
|
|
1668
|
+
"""
|
|
1669
|
+
return self.get_all_tabs()[0]
|
|
1670
|
+
|
|
1671
|
+
def get_last_tab(self) -> ElnExperimentTab:
|
|
1672
|
+
"""
|
|
1673
|
+
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1674
|
+
list of tabs in the experiment and caches them.
|
|
1675
|
+
|
|
1676
|
+
:return: The last tab in the experiment.
|
|
1677
|
+
"""
|
|
1678
|
+
return self.get_all_tabs()[-1]
|
|
1679
|
+
|
|
1680
|
+
def create_tab(self, tab_name: str) -> ElnExperimentTab:
|
|
1681
|
+
"""
|
|
1682
|
+
Create a new tab in the experiment with the input name.
|
|
1683
|
+
|
|
1684
|
+
:param tab_name: The name of the tab to create.
|
|
1685
|
+
:return: The newly created tab.
|
|
1686
|
+
"""
|
|
1687
|
+
crit = ElnExperimentTabAddCriteria(tab_name, [])
|
|
1688
|
+
tab: ElnExperimentTab = self._eln_man.add_tab_for_experiment(self._exp_id, crit)
|
|
1689
|
+
self.add_tab_to_cache(tab)
|
|
1690
|
+
return tab
|
|
1691
|
+
|
|
1692
|
+
def get_tab(self, tab_name: str) -> ElnExperimentTab:
|
|
1693
|
+
"""
|
|
1694
|
+
Return the tab with the input name.
|
|
1695
|
+
|
|
1696
|
+
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1697
|
+
list of tabs in the experiment and caches them.
|
|
1698
|
+
|
|
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]
|
|
1705
|
+
|
|
1706
|
+
def get_steps_in_tab(self, tab: Tab, data_type: DataTypeIdentifier | None = None) -> list[ElnEntryStep]:
|
|
1707
|
+
"""
|
|
1708
|
+
Get all the steps in the input tab sorted in order of appearance.
|
|
1709
|
+
|
|
1710
|
+
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1711
|
+
list of tabs in the experiment and caches them.
|
|
1712
|
+
|
|
1713
|
+
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1714
|
+
and caches them.
|
|
1715
|
+
|
|
1716
|
+
:param tab: The tab or tab name to get the steps of.
|
|
1717
|
+
:param data_type: The data type to filter the steps by. If None, all steps are returned.
|
|
1718
|
+
:return: A list of all the steps in the input tab sorted in order of appearance.
|
|
1719
|
+
"""
|
|
1720
|
+
tab: ElnExperimentTab = self.__to_eln_tab(tab)
|
|
1721
|
+
steps: list[ElnEntryStep] = []
|
|
1722
|
+
for step in self.get_all_steps(data_type):
|
|
1723
|
+
if step.eln_entry.notebook_experiment_tab_id == tab.tab_id:
|
|
1724
|
+
steps.append(step)
|
|
1725
|
+
steps.sort(key=lambda s: (s.eln_entry.order, s.eln_entry.column_order))
|
|
1726
|
+
return steps
|
|
1727
|
+
|
|
1728
|
+
def get_next_entry_order_in_tab(self, tab: Tab) -> int:
|
|
1729
|
+
"""
|
|
1730
|
+
Get the next available order for a new entry in the input tab.
|
|
1731
|
+
|
|
1732
|
+
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1733
|
+
list of tabs in the experiment and caches them.
|
|
1734
|
+
|
|
1735
|
+
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1736
|
+
and caches them.
|
|
1737
|
+
|
|
1738
|
+
:param tab: The tab or tab name to get the steps of.
|
|
1739
|
+
:return: The next available order for a new entry in the input tab.
|
|
1740
|
+
"""
|
|
1741
|
+
steps = self.get_steps_in_tab(tab)
|
|
1742
|
+
return steps[-1].eln_entry.order + 1 if steps else 0
|
|
1743
|
+
|
|
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
|
|
1777
|
+
|
|
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
|
|
1820
|
+
|
|
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:
|
|
2017
|
+
"""
|
|
2018
|
+
Create a new temp data entry in the experiment.
|
|
2019
|
+
|
|
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.
|
|
2025
|
+
"""
|
|
2026
|
+
return self._create_step(ElnEntryType.TempData, entry_name, data_type, position,
|
|
2027
|
+
temp_data_plugin_path=plugin_path)
|
|
2028
|
+
|
|
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.
|
|
2033
|
+
|
|
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.
|
|
2038
|
+
"""
|
|
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
|
|
2073
|
+
|
|
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.
|
|
2082
|
+
"""
|
|
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)
|
|
2102
|
+
|
|
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.
|
|
2110
|
+
"""
|
|
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]
|
|
1205
2134
|
|
|
1206
2135
|
def __to_eln_step(self, step: Step) -> ElnEntryStep:
|
|
1207
2136
|
"""
|
|
@@ -1213,13 +2142,12 @@ class ExperimentHandler:
|
|
|
1213
2142
|
"""
|
|
1214
2143
|
return self.get_step(step) if isinstance(step, str) else step
|
|
1215
2144
|
|
|
1216
|
-
def
|
|
2145
|
+
def __to_eln_tab(self, tab: Tab) -> ElnExperimentTab:
|
|
1217
2146
|
"""
|
|
1218
|
-
|
|
2147
|
+
Convert a variable that could be either a string or an ElnExperimentTab to just an ElnExperimentTab.
|
|
2148
|
+
This will query and cache the tabs for the experiment if the input tab is a name and the tabs have not been
|
|
2149
|
+
cached before.
|
|
1219
2150
|
|
|
1220
|
-
:return: The
|
|
2151
|
+
:return: The input tab as an ElnExperimentTab.
|
|
1221
2152
|
"""
|
|
1222
|
-
if
|
|
1223
|
-
return self.__exp_options
|
|
1224
|
-
self.__exp_options = self.__eln_man.get_notebook_experiment_options(self.__exp_id)
|
|
1225
|
-
return self.__exp_options
|
|
2153
|
+
return self.get_tab(tab) if isinstance(tab, str) else tab
|