sapiopycommons 2025.7.2a578__py3-none-any.whl → 2025.7.2a579__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sapiopycommons might be problematic. Click here for more details.
- sapiopycommons/callbacks/callback_util.py +665 -332
- sapiopycommons/callbacks/field_builder.py +2 -0
- sapiopycommons/chem/IndigoMolecules.py +31 -1
- sapiopycommons/chem/Molecules.py +3 -3
- sapiopycommons/chem/ps_commons.py +381 -0
- sapiopycommons/customreport/auto_pagers.py +26 -1
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/pseudo_data_types.py +349 -326
- sapiopycommons/eln/experiment_cache.py +188 -0
- sapiopycommons/eln/experiment_handler.py +408 -767
- sapiopycommons/eln/experiment_report_util.py +11 -6
- sapiopycommons/eln/experiment_step_factory.py +476 -0
- sapiopycommons/eln/plate_designer.py +7 -2
- sapiopycommons/eln/step_creation.py +236 -0
- sapiopycommons/files/file_util.py +7 -5
- sapiopycommons/general/accession_service.py +2 -2
- sapiopycommons/general/aliases.py +3 -1
- sapiopycommons/general/audit_log.py +7 -0
- sapiopycommons/general/custom_report_util.py +12 -0
- sapiopycommons/general/data_structure_util.py +115 -0
- sapiopycommons/processtracking/custom_workflow_handler.py +11 -1
- sapiopycommons/processtracking/endpoints.py +27 -0
- sapiopycommons/recordmodel/record_handler.py +657 -317
- sapiopycommons/rules/eln_rule_handler.py +8 -1
- sapiopycommons/rules/on_save_rule_handler.py +8 -1
- sapiopycommons/webhook/webhook_handlers.py +3 -0
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.7.2a578.dist-info → sapiopycommons-2025.7.2a579.dist-info}/METADATA +2 -2
- sapiopycommons-2025.7.2a579.dist-info/RECORD +69 -0
- sapiopycommons/ai/__init__.py +0 -0
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.py +0 -43
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.pyi +0 -31
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.py +0 -123
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.pyi +0 -598
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/plan/proto/step_output_pb2.py +0 -45
- sapiopycommons/ai/api/plan/proto/step_output_pb2.pyi +0 -42
- sapiopycommons/ai/api/plan/proto/step_output_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/plan/proto/step_pb2.py +0 -43
- sapiopycommons/ai/api/plan/proto/step_pb2.pyi +0 -43
- sapiopycommons/ai/api/plan/proto/step_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/plan/script/proto/script_pb2.py +0 -53
- sapiopycommons/ai/api/plan/script/proto/script_pb2.pyi +0 -99
- sapiopycommons/ai/api/plan/script/proto/script_pb2_grpc.py +0 -153
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2.py +0 -57
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2.pyi +0 -96
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2.py +0 -67
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2.pyi +0 -220
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2_grpc.py +0 -154
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.py +0 -39
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.pyi +0 -32
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2_grpc.py +0 -24
- sapiopycommons/ai/protobuf_utils.py +0 -508
- sapiopycommons/ai/test_client.py +0 -251
- sapiopycommons/ai/tool_service_base.py +0 -798
- sapiopycommons-2025.7.2a578.dist-info/RECORD +0 -92
- {sapiopycommons-2025.7.2a578.dist-info → sapiopycommons-2025.7.2a579.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.7.2a578.dist-info → sapiopycommons-2025.7.2a579.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,58 +5,46 @@ from collections.abc import Mapping, Iterable
|
|
|
5
5
|
from typing import TypeAlias
|
|
6
6
|
from weakref import WeakValueDictionary
|
|
7
7
|
|
|
8
|
+
from sapiopycommons.eln.experiment_cache import ExperimentCacheManager
|
|
9
|
+
from sapiopycommons.eln.experiment_report_util import ExperimentReportUtil
|
|
10
|
+
from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIdentifier, UserIdentifier, \
|
|
11
|
+
DataTypeIdentifier, RecordModel, ExperimentEntryIdentifier
|
|
12
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
13
|
+
from sapiopycommons.general.time_util import TimeUtil
|
|
14
|
+
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
8
15
|
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
9
|
-
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
10
16
|
from sapiopylib.rest.ELNService import ElnManager
|
|
11
17
|
from sapiopylib.rest.User import SapioUser
|
|
12
18
|
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
19
|
from sapiopylib.rest.pojo.eln.ElnEntryPosition import ElnEntryPosition
|
|
16
20
|
from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment, TemplateExperimentQueryPojo, ElnTemplate, \
|
|
17
21
|
InitializeNotebookExperimentPojo, ElnExperimentUpdateCriteria
|
|
18
22
|
from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry, ExperimentTableEntry, ExperimentFormEntry, \
|
|
19
23
|
ExperimentAttachmentEntry, ExperimentPluginEntry, ExperimentDashboardEntry, ExperimentTextEntry, \
|
|
20
|
-
ExperimentTempDataEntry
|
|
24
|
+
ExperimentTempDataEntry
|
|
21
25
|
from sapiopylib.rest.pojo.eln.ExperimentEntryCriteria import AbstractElnEntryUpdateCriteria, \
|
|
22
26
|
ElnTableEntryUpdateCriteria, ElnFormEntryUpdateCriteria, ElnAttachmentEntryUpdateCriteria, \
|
|
23
27
|
ElnPluginEntryUpdateCriteria, ElnDashboardEntryUpdateCriteria, ElnTextEntryUpdateCriteria, \
|
|
24
|
-
ElnTempDataEntryUpdateCriteria
|
|
28
|
+
ElnTempDataEntryUpdateCriteria
|
|
25
29
|
from sapiopylib.rest.pojo.eln.SapioELNEnums import ExperimentEntryStatus, ElnExperimentStatus, ElnEntryType, \
|
|
26
30
|
ElnBaseDataType
|
|
27
31
|
from sapiopylib.rest.pojo.eln.eln_headings import ElnExperimentTab, ElnExperimentTabAddCriteria
|
|
28
|
-
from sapiopylib.rest.pojo.eln.
|
|
32
|
+
from sapiopylib.rest.pojo.eln.protocol_template import ProtocolTemplateInfo
|
|
29
33
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
30
34
|
from sapiopylib.rest.pojo.webhook.WebhookDirective import ElnExperimentDirective
|
|
31
35
|
from sapiopylib.rest.pojo.webhook.WebhookResult import SapioWebhookResult
|
|
32
36
|
from sapiopylib.rest.utils.Protocols import ElnEntryStep, ElnExperimentProtocol
|
|
33
|
-
from sapiopylib.rest.utils.plates.MultiLayerPlating import MultiLayerPlateConfig, MultiLayerPlateLayer, \
|
|
34
|
-
MultiLayerDataTypeConfig, MultiLayerReplicateConfig, MultiLayerDilutionConfig
|
|
35
|
-
from sapiopylib.rest.utils.plates.MultiLayerPlatingUtils import MultiLayerPlatingManager
|
|
36
|
-
from sapiopylib.rest.utils.plates.PlatingUtils import PlatingOrder
|
|
37
37
|
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
38
38
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelInstanceManager, RecordModelManager
|
|
39
39
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
40
40
|
from sapiopylib.rest.utils.recordmodel.properties import Child
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
from sapiopycommons.general.time_util import TimeUtil
|
|
49
|
-
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
50
|
-
|
|
51
|
-
Step: TypeAlias = str | ElnEntryStep
|
|
52
|
-
"""An object representing an identifier to an ElnEntryStep. May be either the name of the step or the ElnEntryStep
|
|
53
|
-
itself."""
|
|
54
|
-
Tab: TypeAlias = str | ElnExperimentTab
|
|
55
|
-
"""An object representing an identifier to an ElnExperimentTab. May be either the name of the tab or the
|
|
56
|
-
ElnExperimentTab itself."""
|
|
57
|
-
ElnDataTypeFields: TypeAlias = AbstractVeloxFieldDefinition | ElnFieldSetInfo | str | int
|
|
58
|
-
"""An object representing an identifier to an ElnDataType field. These can be field definitions for ad hoc fields,
|
|
59
|
-
predefined field set info objects, predefined field names, or integers for predefined field set IDs."""
|
|
42
|
+
Step: TypeAlias = str | ExperimentEntryIdentifier
|
|
43
|
+
"""An object representing an identifier to an entry in a particular experiment. This may be the name of the experiment,
|
|
44
|
+
or a typical experiment entry identifier."""
|
|
45
|
+
Tab: TypeAlias = int | str | ElnExperimentTab
|
|
46
|
+
"""An object representing an identifier to a tab in a particular experiment. This may be the tab's order, its name,
|
|
47
|
+
or the tab object itself."""
|
|
60
48
|
|
|
61
49
|
|
|
62
50
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
@@ -77,6 +65,8 @@ class ExperimentHandler:
|
|
|
77
65
|
# Managers.
|
|
78
66
|
_eln_man: ElnManager
|
|
79
67
|
"""The ELN manager. Used for updating the experiment and its steps."""
|
|
68
|
+
_exp_cache: ExperimentCacheManager
|
|
69
|
+
"""The experiment cache manager. Used for caching experiment-related information."""
|
|
80
70
|
_inst_man: RecordModelInstanceManager
|
|
81
71
|
"""The record model instance manager. Used for wrapping the data records of a step as record models."""
|
|
82
72
|
_rec_handler: RecordHandler
|
|
@@ -95,7 +85,9 @@ class ExperimentHandler:
|
|
|
95
85
|
|
|
96
86
|
_queried_all_steps: bool
|
|
97
87
|
"""Whether this ExperimentHandler has queried the system for all steps in the experiment."""
|
|
98
|
-
_steps:
|
|
88
|
+
_steps: list[ElnEntryStep]
|
|
89
|
+
"""The sorted list of steps for this experiment. All steps are cached the first time any individual step is accessed."""
|
|
90
|
+
_steps_by_name: dict[str, ElnEntryStep]
|
|
99
91
|
"""Steps from this experiment by their name. All steps are cached the first time any individual step is accessed."""
|
|
100
92
|
_steps_by_id: dict[int, ElnEntryStep]
|
|
101
93
|
"""Steps from this experiment by their ID. All steps are cached the first time any individual step is accessed."""
|
|
@@ -109,13 +101,12 @@ class ExperimentHandler:
|
|
|
109
101
|
_queried_all_tabs: bool
|
|
110
102
|
"""Whether this ExperimentHandler has queried the system for all tabs in the experiment."""
|
|
111
103
|
_tabs: list[ElnExperimentTab]
|
|
112
|
-
"""The tabs for this experiment. Only cached when first accessed."""
|
|
104
|
+
"""The sorted tabs for this experiment. Only cached when first accessed."""
|
|
105
|
+
_tabs_by_id: dict[int, ElnExperimentTab]
|
|
106
|
+
"""The tabs for this experiment by their ID. Only cached when first accessed."""
|
|
113
107
|
_tabs_by_name: dict[str, ElnExperimentTab]
|
|
114
108
|
"""The tabs for this experiment by their name. Only cached when first accessed."""
|
|
115
109
|
|
|
116
|
-
_predefined_fields: dict[str, dict[str, AbstractVeloxFieldDefinition]]
|
|
117
|
-
"""A dictionary of predefined fields for each ELN data type. Only cached when first accessed."""
|
|
118
|
-
|
|
119
110
|
# Constants
|
|
120
111
|
_ENTRY_COMPLETE_STATUSES = [ExperimentEntryStatus.Completed, ExperimentEntryStatus.CompletedApproved]
|
|
121
112
|
"""The set of statuses that an ELN entry could have and be considered completed/submitted."""
|
|
@@ -173,17 +164,20 @@ class ExperimentHandler:
|
|
|
173
164
|
|
|
174
165
|
# Grab various managers that may be used.
|
|
175
166
|
self._eln_man = DataMgmtServer.get_eln_manager(self.user)
|
|
167
|
+
self._exp_cache = ExperimentCacheManager(self.user)
|
|
176
168
|
self._inst_man = RecordModelManager(self.user).instance_manager
|
|
177
169
|
self._rec_handler = RecordHandler(self.user)
|
|
178
170
|
|
|
179
171
|
# Create empty caches to fill when necessary.
|
|
180
172
|
self._queried_all_steps = False
|
|
181
|
-
self.
|
|
173
|
+
self._steps_by_name = {}
|
|
182
174
|
self._steps_by_id = {}
|
|
175
|
+
self._steps = []
|
|
183
176
|
self._step_options = {}
|
|
184
177
|
self._step_updates = {}
|
|
185
178
|
|
|
186
179
|
self._tabs = []
|
|
180
|
+
self._tabs_by_id = {}
|
|
187
181
|
self._tabs_by_name = {}
|
|
188
182
|
|
|
189
183
|
self._queried_all_tabs = False
|
|
@@ -199,7 +193,8 @@ class ExperimentHandler:
|
|
|
199
193
|
for entry in self.context.experiment_entry_list:
|
|
200
194
|
cache_steps.append(ElnEntryStep(self._protocol, entry))
|
|
201
195
|
for step in cache_steps:
|
|
202
|
-
self._steps.
|
|
196
|
+
self._steps.append(step)
|
|
197
|
+
self._steps_by_name.update({step.get_name(): step})
|
|
203
198
|
self._steps_by_id.update({step.get_id(): step})
|
|
204
199
|
|
|
205
200
|
@staticmethod
|
|
@@ -264,6 +259,7 @@ class ExperimentHandler:
|
|
|
264
259
|
"""
|
|
265
260
|
self._queried_all_steps = False
|
|
266
261
|
self._steps.clear()
|
|
262
|
+
self._steps_by_name.clear()
|
|
267
263
|
self._steps_by_id.clear()
|
|
268
264
|
self._step_options.clear()
|
|
269
265
|
self._step_updates.clear()
|
|
@@ -282,32 +278,46 @@ class ExperimentHandler:
|
|
|
282
278
|
"""
|
|
283
279
|
self._queried_all_tabs = False
|
|
284
280
|
self._tabs.clear()
|
|
281
|
+
self._tabs_by_id.clear()
|
|
285
282
|
self._tabs_by_name.clear()
|
|
286
283
|
|
|
287
|
-
def add_entry_to_caches(self, entry: ExperimentEntry | ElnEntryStep) ->
|
|
284
|
+
def add_entry_to_caches(self, entry: ExperimentEntry | ElnEntryStep) -> ElnEntryStep:
|
|
288
285
|
"""
|
|
289
286
|
Add the given entry to the cache of steps for this experiment. This is necessary in order for certain methods to
|
|
290
287
|
work. You should only need to do this if you have created a new entry in your code using a method outside
|
|
291
288
|
of this ExperimentHandler.
|
|
292
289
|
|
|
293
290
|
:param entry: The entry to add to the cache.
|
|
291
|
+
:return: The entry that was added to the cache as an ElnEntryStep.
|
|
294
292
|
"""
|
|
293
|
+
# ExperimentEntries are stored as ElnEntrySteps in the cache.
|
|
295
294
|
if isinstance(entry, ExperimentEntry):
|
|
296
295
|
entry = ElnEntryStep(self._protocol, entry)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
296
|
+
# PR-47699: Confirm that this entry is part of the experiment that this handler is for.
|
|
297
|
+
if entry.eln_entry.parent_experiment_id != self._exp_id:
|
|
298
|
+
raise SapioException(f"Entry with ID {entry.get_id()} is not part of the experiment with ID "
|
|
299
|
+
f"{self._exp_id}.")
|
|
300
|
+
# PR-47699: Don't add the entry if it is already in the cache.
|
|
301
|
+
if entry.get_id() not in self._steps_by_id:
|
|
302
|
+
self._steps.append(entry)
|
|
303
|
+
self._steps_by_name.update({entry.get_name(): entry})
|
|
304
|
+
self._steps_by_id.update({entry.get_id(): entry})
|
|
305
|
+
# Skipping the options cache. The get_step_options method will update that cache when necessary.
|
|
306
|
+
return entry
|
|
307
|
+
|
|
308
|
+
def add_entries_to_caches(self, entries: list[ExperimentEntry | ElnEntryStep]) -> list[ElnEntryStep]:
|
|
302
309
|
"""
|
|
303
310
|
Add the given entries to the cache of steps for this experiment. This is necessary in order for certain methods
|
|
304
311
|
to work. You should only need to do this if you have created a new entry in your code using a method outside
|
|
305
312
|
of this ExperimentHandler.
|
|
306
313
|
|
|
307
314
|
:param entries: The entries to add to the cache.
|
|
315
|
+
:return: The entries that were added to the cache as ElnEntrySteps.
|
|
308
316
|
"""
|
|
317
|
+
new_entries: list[ElnEntryStep] = []
|
|
309
318
|
for entry in entries:
|
|
310
|
-
self.add_entry_to_caches(entry)
|
|
319
|
+
new_entries.append(self.add_entry_to_caches(entry))
|
|
320
|
+
return new_entries
|
|
311
321
|
|
|
312
322
|
def add_tab_to_cache(self, tab: ElnExperimentTab) -> None:
|
|
313
323
|
"""
|
|
@@ -319,12 +329,15 @@ class ExperimentHandler:
|
|
|
319
329
|
"""
|
|
320
330
|
self._tabs.append(tab)
|
|
321
331
|
self._tabs.sort(key=lambda t: t.tab_order)
|
|
332
|
+
self._tabs_by_id[tab.tab_id] = tab
|
|
322
333
|
self._tabs_by_name[tab.tab_name] = tab
|
|
323
334
|
|
|
324
335
|
# FR-46495: Split the creation of the experiment in launch_experiment into a create_experiment function.
|
|
336
|
+
# CR-47703: Allow create_experiment and launch_experiment to accept None as a template_name to create a blank
|
|
337
|
+
# experiment. Also allow a SapioUser object to be provided as context instead of a full SapioWebhookContext object.
|
|
325
338
|
@staticmethod
|
|
326
|
-
def create_experiment(context:
|
|
327
|
-
template_name: str,
|
|
339
|
+
def create_experiment(context: UserIdentifier,
|
|
340
|
+
template_name: str | None = None,
|
|
328
341
|
experiment_name: str | None = None,
|
|
329
342
|
parent_record: SapioRecord | None = None, *,
|
|
330
343
|
template_version: int | None = None, active_templates_only: bool = True) -> ElnExperiment:
|
|
@@ -335,8 +348,9 @@ class ExperimentHandler:
|
|
|
335
348
|
templates match the same criteria, the first template that is encountered in the query is used. Throws an
|
|
336
349
|
exception if no template is found. Also makes a webservice request to create the experiment.
|
|
337
350
|
|
|
338
|
-
:param context: The current webhook context.
|
|
339
|
-
:param template_name: The name of the template to create the experiment from.
|
|
351
|
+
:param context: The current webhook context or a user object to send requests from.
|
|
352
|
+
:param template_name: The name of the template to create the experiment from. If None, a blank experiment
|
|
353
|
+
is created.
|
|
340
354
|
:param experiment_name: The name to give to the experiment after it is created. If not provided, defaults to the
|
|
341
355
|
display name of the template.
|
|
342
356
|
:param parent_record: The parent record to attach this experiment under. This record must be an eligible
|
|
@@ -348,32 +362,27 @@ class ExperimentHandler:
|
|
|
348
362
|
:param active_templates_only: Whether only active templates should be queried for.
|
|
349
363
|
:return: The newly created experiment.
|
|
350
364
|
"""
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
("" if template_version is None else f" and the version {template_version}") +
|
|
365
|
-
f" found.")
|
|
366
|
-
|
|
367
|
-
if experiment_name is None:
|
|
368
|
-
experiment_name: str = launch_template.display_name
|
|
365
|
+
user = AliasUtil.to_sapio_user(context)
|
|
366
|
+
cache = ExperimentCacheManager(user)
|
|
367
|
+
eln_manager: ElnManager = DataMgmtServer.get_eln_manager(user)
|
|
368
|
+
|
|
369
|
+
template_id: int | None = None
|
|
370
|
+
if template_name:
|
|
371
|
+
launch_template: ElnTemplate = cache.get_experiment_template(template_name, active_templates_only,
|
|
372
|
+
template_version, first_match=True)
|
|
373
|
+
template_id = launch_template.template_id
|
|
374
|
+
if experiment_name is None:
|
|
375
|
+
experiment_name: str = launch_template.display_name
|
|
376
|
+
elif experiment_name is None:
|
|
377
|
+
experiment_name = f"{user.username}'s Experiment"
|
|
369
378
|
if parent_record is not None:
|
|
370
379
|
parent_record: DataRecord = AliasUtil.to_data_record(parent_record)
|
|
371
|
-
notebook_init = InitializeNotebookExperimentPojo(experiment_name,
|
|
372
|
-
return
|
|
380
|
+
notebook_init = InitializeNotebookExperimentPojo(experiment_name, template_id, parent_record)
|
|
381
|
+
return eln_manager.create_notebook_experiment(notebook_init)
|
|
373
382
|
|
|
374
383
|
@staticmethod
|
|
375
|
-
def launch_experiment(context:
|
|
376
|
-
template_name: str,
|
|
384
|
+
def launch_experiment(context: UserIdentifier,
|
|
385
|
+
template_name: str | None = None,
|
|
377
386
|
experiment_name: str | None = None,
|
|
378
387
|
parent_record: SapioRecord | None = None, *,
|
|
379
388
|
template_version: int | None = None,
|
|
@@ -386,8 +395,9 @@ class ExperimentHandler:
|
|
|
386
395
|
templates match the same criteria, the first template that is encountered in the query is used. Throws an
|
|
387
396
|
exception if no template is found. Also makes a webservice request to create the experiment.
|
|
388
397
|
|
|
389
|
-
:param context: The current webhook context.
|
|
390
|
-
:param template_name: The name of the template to create the experiment from.
|
|
398
|
+
:param context: The current webhook context or a user object to send requests from.
|
|
399
|
+
:param template_name: The name of the template to create the experiment from. If None, a blank experiment
|
|
400
|
+
is created.
|
|
391
401
|
:param experiment_name: The name to give to the experiment after it is created. If not provided, defaults to the
|
|
392
402
|
display name of the template.
|
|
393
403
|
:param parent_record: The parent record to attach this experiment under. This record must be an eligible
|
|
@@ -590,6 +600,9 @@ class ExperimentHandler:
|
|
|
590
600
|
"""
|
|
591
601
|
Set the experiment's status to Completed. Makes a webservice call to update the experiment. Checks if the
|
|
592
602
|
experiment is already completed, and does nothing if so.
|
|
603
|
+
|
|
604
|
+
NOTE: This will cause the usual process tracking logic to run as if you'd clicked the "Complete Experiment"
|
|
605
|
+
toolbar button. This includes moving the in process samples forward to the next step in the process.
|
|
593
606
|
"""
|
|
594
607
|
if not self.experiment_is_complete():
|
|
595
608
|
self._protocol.complete_protocol()
|
|
@@ -600,9 +613,11 @@ class ExperimentHandler:
|
|
|
600
613
|
Set the experiment's status to Canceled. Makes a webservice call to update the experiment. Checks if the
|
|
601
614
|
experiment is already canceled, and does nothing if so.
|
|
602
615
|
|
|
603
|
-
NOTE: This will
|
|
604
|
-
|
|
605
|
-
|
|
616
|
+
NOTE: This will cause the usual process tracking logic to run as if you'd clicked the "Cancel Experiment"
|
|
617
|
+
toolbar button. This includes moving the in process samples back into the process queue for the current step.
|
|
618
|
+
|
|
619
|
+
On version 24.12 and earlier, this was not the case, as the process tracking logic was tied to the button
|
|
620
|
+
instead of being on the experiment status change.
|
|
606
621
|
"""
|
|
607
622
|
if not self.experiment_is_canceled():
|
|
608
623
|
self._protocol.cancel_protocol()
|
|
@@ -632,14 +647,14 @@ class ExperimentHandler:
|
|
|
632
647
|
"""
|
|
633
648
|
return all([x is not None for x in self.get_steps(step_names, False)])
|
|
634
649
|
|
|
635
|
-
def get_step(self, step_name:
|
|
650
|
+
def get_step(self, step_name: Step, exception_on_none: bool = True) -> ElnEntryStep | None:
|
|
636
651
|
"""
|
|
637
652
|
Get the step of the given name from the experiment.
|
|
638
653
|
|
|
639
654
|
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
640
655
|
list of steps in the experiment and caches them.
|
|
641
656
|
|
|
642
|
-
:param step_name: The
|
|
657
|
+
:param step_name: The identifier for the step to return.
|
|
643
658
|
:param exception_on_none: If false, returns None if the entry can't be found. If true, raises an exception
|
|
644
659
|
when the named entry doesn't exist in the experiment.
|
|
645
660
|
:return: An ElnEntrySteps matching the provided name. If there is no match and no exception is to be thrown,
|
|
@@ -647,31 +662,49 @@ class ExperimentHandler:
|
|
|
647
662
|
"""
|
|
648
663
|
return self.get_steps([step_name], exception_on_none)[0]
|
|
649
664
|
|
|
650
|
-
def get_steps(self, step_names: Iterable[
|
|
665
|
+
def get_steps(self, step_names: Iterable[Step], exception_on_none: bool = True) -> list[ElnEntryStep | None]:
|
|
651
666
|
"""
|
|
652
667
|
Get a list of steps of the given names from the experiment, sorted in the same order as the names are provided.
|
|
653
668
|
|
|
654
669
|
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
655
670
|
list of steps in the experiment and caches them.
|
|
656
671
|
|
|
657
|
-
:param step_names: A list of
|
|
672
|
+
:param step_names: A list of identifiers for the entries to return and the order to return them in.
|
|
658
673
|
:param exception_on_none: If false, returns None for entries that can't be found. If true, raises an exception
|
|
659
674
|
when the named entry doesn't exist in the experiment.
|
|
660
675
|
:return: A list of ElnEntrySteps matching the provided names in the order they were provided in. If there is no
|
|
661
676
|
match for a given step and no exception is to be thrown, returns None for that step.
|
|
662
677
|
"""
|
|
678
|
+
# CR-47700: Support getting steps from entry objects.
|
|
679
|
+
step_ids: list[str | int] = []
|
|
680
|
+
for step in step_names:
|
|
681
|
+
# Convert any ElnEntrySteps into ExperimentEntries.
|
|
682
|
+
if isinstance(step, ElnEntryStep):
|
|
683
|
+
step: ExperimentEntry = step.eln_entry
|
|
684
|
+
# If an ExperimentEntry is provided, then add its ID to the list of steps to return.
|
|
685
|
+
if isinstance(step, ExperimentEntry):
|
|
686
|
+
step_ids.append(step.entry_id)
|
|
687
|
+
# Add this entry to the caches so that it doesn't need to be queried for. This will also verify that
|
|
688
|
+
# this entry belongs to the experiment that this handler is for.
|
|
689
|
+
self.add_entry_to_caches(step)
|
|
690
|
+
elif isinstance(step, (str, int)):
|
|
691
|
+
step_ids.append(step)
|
|
692
|
+
|
|
663
693
|
ret_list: list[ElnEntryStep | None] = []
|
|
664
|
-
for
|
|
694
|
+
for step_id in step_ids:
|
|
665
695
|
# If we haven't queried the system for all steps in the experiment yet, then the reason that a step is
|
|
666
696
|
# missing may be because it wasn't in the webhook context. Therefore, query all steps and then check
|
|
667
697
|
# if the step name is still missing from the experiment before potentially throwing an exception.
|
|
668
|
-
if
|
|
669
|
-
self.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
698
|
+
if not self._queried_all_steps:
|
|
699
|
+
if ((isinstance(step_id, str) and step_id not in self._steps_by_name)
|
|
700
|
+
or (isinstance(step_id, int) and step_id not in self._steps_by_id)):
|
|
701
|
+
self._query_all_steps()
|
|
702
|
+
if isinstance(step_id, str):
|
|
703
|
+
step: ElnEntryStep = self._steps_by_name.get(step_id)
|
|
704
|
+
else:
|
|
705
|
+
step: ElnEntryStep = self._steps_by_id.get(step_id)
|
|
673
706
|
if step is None and exception_on_none is True:
|
|
674
|
-
raise SapioException(f"ElnEntryStep of name \"{
|
|
707
|
+
raise SapioException(f"ElnEntryStep of name \"{step_id}\" not found in experiment with ID {self._exp_id}.")
|
|
675
708
|
ret_list.append(step)
|
|
676
709
|
return ret_list
|
|
677
710
|
|
|
@@ -686,14 +719,34 @@ class ExperimentHandler:
|
|
|
686
719
|
:return: Every entry in the experiment in order of appearance that match the provided data type, if any.
|
|
687
720
|
"""
|
|
688
721
|
if self._queried_all_steps is False:
|
|
689
|
-
self.
|
|
690
|
-
|
|
691
|
-
|
|
722
|
+
self._query_all_steps()
|
|
723
|
+
else:
|
|
724
|
+
# Re-sort the steps in case any new steps were added before the last time that this was called.
|
|
725
|
+
def sort_steps(step: ElnEntryStep) -> tuple:
|
|
726
|
+
entry = step.eln_entry
|
|
727
|
+
tab_order: int = self.get_tab_for_step(step).tab_order
|
|
728
|
+
entry_order: int = entry.order
|
|
729
|
+
column_order: int = entry.column_order
|
|
730
|
+
return tab_order, entry_order, column_order
|
|
731
|
+
|
|
732
|
+
self._steps.sort(key=sort_steps)
|
|
733
|
+
all_steps: list[ElnEntryStep] = self._steps
|
|
692
734
|
if data_type is None:
|
|
693
735
|
return all_steps
|
|
694
736
|
data_type: str = AliasUtil.to_data_type_name(data_type)
|
|
695
737
|
return [x for x in all_steps if data_type in x.get_data_type_names()]
|
|
696
738
|
|
|
739
|
+
def _query_all_steps(self) -> None:
|
|
740
|
+
"""
|
|
741
|
+
Query the system for every step in the experiment and cache them.
|
|
742
|
+
"""
|
|
743
|
+
self._queried_all_steps = True
|
|
744
|
+
self._protocol.invalidate()
|
|
745
|
+
self._steps = self._protocol.get_sorted_step_list()
|
|
746
|
+
for step in self._steps:
|
|
747
|
+
self._steps_by_name[step.get_name()] = step
|
|
748
|
+
self._steps_by_id[step.get_id()] = step
|
|
749
|
+
|
|
697
750
|
def get_step_by_option(self, key: str, value: str | None = None) -> ElnEntryStep:
|
|
698
751
|
"""
|
|
699
752
|
Retrieve the step in this experiment that contains an entry option with the provided key and value.
|
|
@@ -740,7 +793,7 @@ class ExperimentHandler:
|
|
|
740
793
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
741
794
|
:return: The data records for the given step.
|
|
742
795
|
"""
|
|
743
|
-
return self.
|
|
796
|
+
return self.get_step(step).get_records()
|
|
744
797
|
|
|
745
798
|
def get_step_models(self, step: Step, wrapper_type: type[WrappedType] | None = None) \
|
|
746
799
|
-> list[WrappedType] | list[PyRecordModel]:
|
|
@@ -777,13 +830,14 @@ class ExperimentHandler:
|
|
|
777
830
|
A list of records to add to the given step.
|
|
778
831
|
The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
|
|
779
832
|
"""
|
|
780
|
-
step = self.
|
|
833
|
+
step: ElnEntryStep = self.get_step(step)
|
|
781
834
|
if not records:
|
|
782
835
|
return
|
|
783
836
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
784
837
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
785
|
-
raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. "
|
|
786
|
-
f"
|
|
838
|
+
raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. ELN "
|
|
839
|
+
f"records that are committed to the system will automatically appear in the ELN entry "
|
|
840
|
+
f"with the matching data type name.")
|
|
787
841
|
if dt != step.get_data_type_names()[0]:
|
|
788
842
|
raise SapioException(f"Cannot add {dt} records to entry {step.get_name()} of type "
|
|
789
843
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -791,8 +845,7 @@ class ExperimentHandler:
|
|
|
791
845
|
|
|
792
846
|
def remove_step_records(self, step: Step, records: Iterable[SapioRecord]) -> None:
|
|
793
847
|
"""
|
|
794
|
-
Make a webservice call to remove a list of records from a step.
|
|
795
|
-
entries. For removing from an ELN data type table entry, see remove_eln_rows.
|
|
848
|
+
Make a webservice call to remove a list of records from a step.
|
|
796
849
|
|
|
797
850
|
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
798
851
|
list of steps in the experiment and caches them.
|
|
@@ -804,13 +857,14 @@ class ExperimentHandler:
|
|
|
804
857
|
A list of records to remove from the given step.
|
|
805
858
|
The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
|
|
806
859
|
"""
|
|
807
|
-
step = self.
|
|
860
|
+
step: ElnEntryStep = self.get_step(step)
|
|
808
861
|
if not records:
|
|
809
862
|
return
|
|
810
863
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
811
864
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
812
|
-
|
|
813
|
-
|
|
865
|
+
# CR-47532: Add remove_step_records support for Experiment Detail and Sample Detail entries.
|
|
866
|
+
self.remove_eln_rows(step, records)
|
|
867
|
+
return
|
|
814
868
|
if dt != step.get_data_type_names()[0]:
|
|
815
869
|
raise SapioException(f"Cannot remove {dt} records from entry {step.get_name()} of type "
|
|
816
870
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -836,12 +890,17 @@ class ExperimentHandler:
|
|
|
836
890
|
A list of records to set for the given step,
|
|
837
891
|
The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
|
|
838
892
|
"""
|
|
839
|
-
step = self.
|
|
893
|
+
step: ElnEntryStep = self.get_step(step)
|
|
840
894
|
if records:
|
|
841
895
|
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
896
|
+
# CR-47532: Add set_step_records support for Experiment Detail and Sample Detail entries.
|
|
842
897
|
if ElnBaseDataType.is_base_data_type(dt):
|
|
843
|
-
|
|
844
|
-
|
|
898
|
+
remove_rows: list[PyRecordModel] = []
|
|
899
|
+
for record in self.get_step_models(step):
|
|
900
|
+
if record not in records:
|
|
901
|
+
remove_rows.append(record)
|
|
902
|
+
self.remove_eln_rows(step, remove_rows)
|
|
903
|
+
return
|
|
845
904
|
if dt != step.get_data_type_names()[0]:
|
|
846
905
|
raise SapioException(f"Cannot set {dt} records for entry {step.get_name()} of type "
|
|
847
906
|
f"{step.get_data_type_names()[0]}.")
|
|
@@ -863,11 +922,28 @@ class ExperimentHandler:
|
|
|
863
922
|
The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
|
|
864
923
|
"""
|
|
865
924
|
self.set_step_records(step, [record])
|
|
866
|
-
step = self.
|
|
925
|
+
step: ElnEntryStep = self.get_step(step)
|
|
867
926
|
if isinstance(step.eln_entry, ExperimentFormEntry):
|
|
868
927
|
step.eln_entry.record_id = AliasUtil.to_data_record(record).record_id
|
|
869
928
|
|
|
870
929
|
# FR-46496 - Provide functions for adding and removing rows from an ELN data type entry.
|
|
930
|
+
def add_eln_row(self, step: Step, wrapper_type: type[WrappedType] | None = None) -> WrappedType | PyRecordModel:
|
|
931
|
+
"""
|
|
932
|
+
Add a row to an ELNExperimentDetail or ELNSampleDetail table entry. The row will not appear in the system
|
|
933
|
+
until a record manager store and commit has occurred.
|
|
934
|
+
|
|
935
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
936
|
+
list of steps in the experiment and caches them.
|
|
937
|
+
|
|
938
|
+
:param step:
|
|
939
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
940
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
941
|
+
:param wrapper_type: Optionally wrap the ELN data type in a record model wrapper. If not provided, returns
|
|
942
|
+
an unwrapped PyRecordModel.
|
|
943
|
+
:return: The newly created row.
|
|
944
|
+
"""
|
|
945
|
+
return self.add_eln_rows(step, 1, wrapper_type)[0]
|
|
946
|
+
|
|
871
947
|
def add_eln_rows(self, step: Step, count: int, wrapper_type: type[WrappedType] | None = None) \
|
|
872
948
|
-> list[WrappedType] | list[PyRecordModel]:
|
|
873
949
|
"""
|
|
@@ -885,7 +961,7 @@ class ExperimentHandler:
|
|
|
885
961
|
an unwrapped PyRecordModel.
|
|
886
962
|
:return: A list of the newly created rows.
|
|
887
963
|
"""
|
|
888
|
-
step = self.
|
|
964
|
+
step: ElnEntryStep = self.get_step(step)
|
|
889
965
|
if step.eln_entry.entry_type != ElnEntryType.Table:
|
|
890
966
|
raise SapioException("The provided step is not a table entry.")
|
|
891
967
|
dt: str = step.get_data_type_names()[0]
|
|
@@ -896,10 +972,64 @@ class ExperimentHandler:
|
|
|
896
972
|
return self._inst_man.wrap_list(records, wrapper_type)
|
|
897
973
|
return records
|
|
898
974
|
|
|
899
|
-
def
|
|
975
|
+
def add_sample_detail(self, step: Step, sample: RecordModel,
|
|
976
|
+
wrapper_type: type[WrappedType] | None = None) \
|
|
977
|
+
-> WrappedType | PyRecordModel:
|
|
900
978
|
"""
|
|
901
|
-
Add a
|
|
902
|
-
|
|
979
|
+
Add a sample detail to a sample detail entry while relating it to the input sample record.
|
|
980
|
+
|
|
981
|
+
:param step:
|
|
982
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
983
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
984
|
+
:param sample: The sample record to add the sample detail to.
|
|
985
|
+
:param wrapper_type: Optionally wrap the sample detail in a record model wrapper. If not provided, returns
|
|
986
|
+
an unwrapped PyRecordModel.
|
|
987
|
+
:return: The newly created sample detail.
|
|
988
|
+
"""
|
|
989
|
+
return self.add_sample_details(step, [sample], wrapper_type)[0]
|
|
990
|
+
|
|
991
|
+
def add_sample_details(self, step: Step, samples: Iterable[RecordModel],
|
|
992
|
+
wrapper_type: type[WrappedType] | None = None) \
|
|
993
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
994
|
+
"""
|
|
995
|
+
Add sample details to a sample details entry while relating them to the input sample records.
|
|
996
|
+
|
|
997
|
+
:param step:
|
|
998
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
999
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1000
|
+
:param samples: The sample records to add the sample details to.
|
|
1001
|
+
:param wrapper_type: Optionally wrap the sample details in a record model wrapper. If not provided, returns
|
|
1002
|
+
an unwrapped PyRecordModel.
|
|
1003
|
+
:return: The newly created sample details. The indices of the samples in the input list match the index of the
|
|
1004
|
+
sample details in this list that they are related to.
|
|
1005
|
+
"""
|
|
1006
|
+
step: ElnEntryStep = self.get_step(step)
|
|
1007
|
+
if step.eln_entry.entry_type != ElnEntryType.Table:
|
|
1008
|
+
raise SapioException("The provided step is not a table entry.")
|
|
1009
|
+
dt: str = step.get_data_type_names()[0]
|
|
1010
|
+
if not ElnBaseDataType.is_eln_type(dt) or ElnBaseDataType.get_base_type(dt) != ElnBaseDataType.SAMPLE_DETAIL:
|
|
1011
|
+
raise SapioException("The provided step is not an ELNSampleDetail entry.")
|
|
1012
|
+
records: list[PyRecordModel] = []
|
|
1013
|
+
for sample in samples:
|
|
1014
|
+
if sample.data_type_name != "Sample":
|
|
1015
|
+
raise SapioException(f"Received a {sample.data_type_name} record when Sample records were expected.")
|
|
1016
|
+
detail: PyRecordModel = sample.add(Child.create_by_name(dt))
|
|
1017
|
+
detail.set_field_values({
|
|
1018
|
+
"SampleId": sample.get_field_value("SampleId"),
|
|
1019
|
+
"OtherSampleId": sample.get_field_value("OtherSampleId")
|
|
1020
|
+
})
|
|
1021
|
+
records.append(detail)
|
|
1022
|
+
if wrapper_type:
|
|
1023
|
+
return self._inst_man.wrap_list(records, wrapper_type)
|
|
1024
|
+
return records
|
|
1025
|
+
|
|
1026
|
+
def remove_eln_row(self, step: Step, record: SapioRecord) -> None:
|
|
1027
|
+
"""
|
|
1028
|
+
Remove a row from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
|
|
1029
|
+
records in the system that match the entry's data type. This means that removing rows from an ELN data type
|
|
1030
|
+
table entry is equivalent to deleting the records for the rows.
|
|
1031
|
+
|
|
1032
|
+
The row will not be deleted in the system until a record manager store and commit has occurred.
|
|
903
1033
|
|
|
904
1034
|
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
905
1035
|
list of steps in the experiment and caches them.
|
|
@@ -907,13 +1037,13 @@ class ExperimentHandler:
|
|
|
907
1037
|
:param step:
|
|
908
1038
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
909
1039
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
910
|
-
:param
|
|
911
|
-
|
|
912
|
-
|
|
1040
|
+
:param record:
|
|
1041
|
+
The record to remove from the given step.
|
|
1042
|
+
The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
|
|
913
1043
|
"""
|
|
914
|
-
|
|
1044
|
+
self.remove_eln_rows(step, [record])
|
|
915
1045
|
|
|
916
|
-
def remove_eln_rows(self, step: Step, records:
|
|
1046
|
+
def remove_eln_rows(self, step: Step, records: Iterable[SapioRecord]) -> None:
|
|
917
1047
|
"""
|
|
918
1048
|
Remove rows from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
|
|
919
1049
|
records in the system that match the entry's data type. This means that removing rows from an ELN data type
|
|
@@ -931,7 +1061,7 @@ class ExperimentHandler:
|
|
|
931
1061
|
A list of records to remove from the given step.
|
|
932
1062
|
The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
|
|
933
1063
|
"""
|
|
934
|
-
step = self.
|
|
1064
|
+
step: ElnEntryStep = self.get_step(step)
|
|
935
1065
|
dt: str = step.get_data_type_names()[0]
|
|
936
1066
|
if not ElnBaseDataType.is_eln_type(dt):
|
|
937
1067
|
raise SapioException("The provided step is not an ELN data type entry.")
|
|
@@ -954,61 +1084,6 @@ class ExperimentHandler:
|
|
|
954
1084
|
for record in record_models:
|
|
955
1085
|
record.delete()
|
|
956
1086
|
|
|
957
|
-
def remove_eln_row(self, step: Step, record: SapioRecord) -> None:
|
|
958
|
-
"""
|
|
959
|
-
Remove a row from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
|
|
960
|
-
records in the system that match the entry's data type. This means that removing rows from an ELN data type
|
|
961
|
-
table entry is equivalent to deleting the records for the rows.
|
|
962
|
-
|
|
963
|
-
The row will not be deleted in the system until a record manager store and commit has occurred.
|
|
964
|
-
|
|
965
|
-
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
966
|
-
list of steps in the experiment and caches them.
|
|
967
|
-
|
|
968
|
-
:param step:
|
|
969
|
-
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
970
|
-
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
971
|
-
:param record:
|
|
972
|
-
The record to remove from the given step.
|
|
973
|
-
The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
|
|
974
|
-
"""
|
|
975
|
-
self.remove_eln_rows(step, [record])
|
|
976
|
-
|
|
977
|
-
def add_sample_details(self, step: Step, samples: list[RecordModel],
|
|
978
|
-
wrapper_type: type[WrappedType] | None = None) \
|
|
979
|
-
-> list[WrappedType] | list[PyRecordModel]:
|
|
980
|
-
"""
|
|
981
|
-
Add sample details to a sample details entry while relating them to the input sample records.
|
|
982
|
-
|
|
983
|
-
:param step:
|
|
984
|
-
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
985
|
-
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
986
|
-
:param samples: The sample records to add the sample details to.
|
|
987
|
-
:param wrapper_type: Optionally wrap the sample details in a record model wrapper. If not provided, returns
|
|
988
|
-
an unwrapped PyRecordModel.
|
|
989
|
-
:return: The newly created sample details. The indices of the samples in the input list match the index of the
|
|
990
|
-
sample details in this list that they are related to.
|
|
991
|
-
"""
|
|
992
|
-
step = self.__to_eln_step(step)
|
|
993
|
-
if step.eln_entry.entry_type != ElnEntryType.Table:
|
|
994
|
-
raise SapioException("The provided step is not a table entry.")
|
|
995
|
-
dt: str = step.get_data_type_names()[0]
|
|
996
|
-
if not ElnBaseDataType.is_eln_type(dt) or ElnBaseDataType.get_base_type(dt) != ElnBaseDataType.SAMPLE_DETAIL:
|
|
997
|
-
raise SapioException("The provided step is not an ELNSampleDetail entry.")
|
|
998
|
-
records: list[PyRecordModel] = []
|
|
999
|
-
for sample in samples:
|
|
1000
|
-
if sample.data_type_name != "Sample":
|
|
1001
|
-
raise SapioException(f"Received a {sample.data_type_name} record when Sample records were expected.")
|
|
1002
|
-
detail: PyRecordModel = sample.add(Child.create_by_name(dt))
|
|
1003
|
-
detail.set_field_values({
|
|
1004
|
-
"SampleId": sample.get_field_value("SampleId"),
|
|
1005
|
-
"OtherSampleId": sample.get_field_value("OtherSampleId")
|
|
1006
|
-
})
|
|
1007
|
-
records.append(detail)
|
|
1008
|
-
if wrapper_type:
|
|
1009
|
-
return self._inst_man.wrap_list(records, wrapper_type)
|
|
1010
|
-
return records
|
|
1011
|
-
|
|
1012
1087
|
# noinspection PyPep8Naming
|
|
1013
1088
|
def update_step(self, step: Step,
|
|
1014
1089
|
entry_name: str | None = None,
|
|
@@ -1076,128 +1151,45 @@ class ExperimentHandler:
|
|
|
1076
1151
|
If you wish to add options to the existing map of options that an entry has, use the
|
|
1077
1152
|
add_step_options method.
|
|
1078
1153
|
"""
|
|
1079
|
-
# FR-47468: Deprecating this since the
|
|
1080
|
-
|
|
1081
|
-
warnings.warn("Update step is deprecated. Use force_entry_update_params instead.",
|
|
1154
|
+
# FR-47468: Deprecating this since the entry-specific update criteria should be used instead.
|
|
1155
|
+
warnings.warn("Update step is deprecated. Use force_entry_update instead.",
|
|
1082
1156
|
DeprecationWarning)
|
|
1083
|
-
self.
|
|
1084
|
-
|
|
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.
|
|
1157
|
+
step: ElnEntryStep = self.get_step(step)
|
|
1158
|
+
update = AbstractElnEntryUpdateCriteria(step.eln_entry.entry_type)
|
|
1134
1159
|
|
|
1135
|
-
|
|
1160
|
+
# These two variables could be iterables that aren't lists. Convert them to plain
|
|
1161
|
+
# lists, since that's what the update criteria is expecting.
|
|
1162
|
+
if related_entry_set is not None:
|
|
1163
|
+
related_entry_set = list(related_entry_set)
|
|
1164
|
+
if dependency_set is not None:
|
|
1165
|
+
dependency_set = list(dependency_set)
|
|
1136
1166
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1167
|
+
update.entry_name = entry_name
|
|
1168
|
+
update.related_entry_set = related_entry_set
|
|
1169
|
+
update.dependency_set = dependency_set
|
|
1170
|
+
update.entry_status = entry_status
|
|
1171
|
+
update.order = order
|
|
1172
|
+
update.description = description
|
|
1173
|
+
update.requires_grabber_plugin = requires_grabber_plugin
|
|
1174
|
+
update.is_initialization_required = is_initialization_required
|
|
1175
|
+
update.notebook_experiment_tab_id = notebook_experiment_tab_id
|
|
1176
|
+
update.entry_height = entry_height
|
|
1177
|
+
update.column_order = column_order
|
|
1178
|
+
update.column_span = column_span
|
|
1179
|
+
update.is_removable = is_removable
|
|
1180
|
+
update.is_renamable = is_renamable
|
|
1181
|
+
update.source_entry_id = source_entry_id
|
|
1182
|
+
update.clear_source_entry_id = clear_source_entry_id
|
|
1183
|
+
update.is_hidden = is_hidden
|
|
1184
|
+
update.is_static_View = is_static_View
|
|
1185
|
+
update.is_shown_in_template = is_shown_in_template
|
|
1186
|
+
update.template_item_fulfilled_timestamp = template_item_fulfilled_timestamp
|
|
1187
|
+
update.clear_template_item_fulfilled_timestamp = clear_template_item_fulfilled_timestamp
|
|
1188
|
+
update.entry_options_map = entry_options_map
|
|
1139
1189
|
|
|
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
1190
|
self.force_step_update(step, update)
|
|
1200
1191
|
|
|
1192
|
+
# FR-47468: Some functions that can help with entry updates.
|
|
1201
1193
|
def force_step_update(self, step: Step, update: AbstractElnEntryUpdateCriteria) -> None:
|
|
1202
1194
|
"""
|
|
1203
1195
|
Immediately sent an update to an entry in this experiment. All changes will be reflected by the ExperimentEntry
|
|
@@ -1213,7 +1205,7 @@ class ExperimentHandler:
|
|
|
1213
1205
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1214
1206
|
:param update: The update to make to the step.
|
|
1215
1207
|
"""
|
|
1216
|
-
step = self.
|
|
1208
|
+
step: ElnEntryStep = self.get_step(step)
|
|
1217
1209
|
self._eln_man.update_experiment_entry(self._exp_id, step.get_id(), update)
|
|
1218
1210
|
self._update_entry_details(step, update)
|
|
1219
1211
|
|
|
@@ -1233,7 +1225,7 @@ class ExperimentHandler:
|
|
|
1233
1225
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1234
1226
|
:param update: The update to make to the step.
|
|
1235
1227
|
"""
|
|
1236
|
-
step = self.
|
|
1228
|
+
step: ElnEntryStep = self.get_step(step)
|
|
1237
1229
|
if step.eln_entry.entry_type != update.entry_type:
|
|
1238
1230
|
raise SapioException(f"The provided step and update criteria are not of the same entry type. "
|
|
1239
1231
|
f"The step is of type {step.eln_entry.entry_type} and the update criteria is of type "
|
|
@@ -1264,72 +1256,13 @@ class ExperimentHandler:
|
|
|
1264
1256
|
Commit all the stored updates to the entries in this experiment. The updates are made in the order that they
|
|
1265
1257
|
were stored.
|
|
1266
1258
|
"""
|
|
1259
|
+
if not self._step_updates:
|
|
1260
|
+
return
|
|
1267
1261
|
self._eln_man.update_experiment_entries(self._exp_id, self._step_updates)
|
|
1268
1262
|
for step_id, criteria in self._step_updates.items():
|
|
1269
1263
|
self._update_entry_details(self._steps_by_id[step_id], criteria)
|
|
1270
1264
|
self._step_updates.clear()
|
|
1271
1265
|
|
|
1272
|
-
def _criteria_from_params(self, step: Step, *,
|
|
1273
|
-
entry_name: str | None = None,
|
|
1274
|
-
related_entry_set: Iterable[int] | None = None,
|
|
1275
|
-
dependency_set: Iterable[int] | None = None,
|
|
1276
|
-
entry_status: ExperimentEntryStatus | None = None,
|
|
1277
|
-
order: int | None = None,
|
|
1278
|
-
description: str | None = None,
|
|
1279
|
-
requires_grabber_plugin: bool | None = None,
|
|
1280
|
-
is_initialization_required: bool | None = None,
|
|
1281
|
-
notebook_experiment_tab_id: int | None = None,
|
|
1282
|
-
entry_height: int | None = None,
|
|
1283
|
-
column_order: int | None = None,
|
|
1284
|
-
column_span: int | None = None,
|
|
1285
|
-
is_removable: bool | None = None,
|
|
1286
|
-
is_renamable: bool | None = None,
|
|
1287
|
-
source_entry_id: int | None = None,
|
|
1288
|
-
clear_source_entry_id: bool | None = None,
|
|
1289
|
-
is_hidden: bool | None = None,
|
|
1290
|
-
is_static_view: bool | None = None,
|
|
1291
|
-
is_shown_in_template: bool | None = None,
|
|
1292
|
-
template_item_fulfilled_timestamp: int | None = None,
|
|
1293
|
-
clear_template_item_fulfilled_timestamp: bool | None = None,
|
|
1294
|
-
entry_options_map: dict[str, str] | None = None) -> AbstractElnEntryUpdateCriteria:
|
|
1295
|
-
"""
|
|
1296
|
-
Create an abstract update criteria object from the provided parameters for the given step.
|
|
1297
|
-
"""
|
|
1298
|
-
step: ElnEntryStep = self.__to_eln_step(step)
|
|
1299
|
-
update = AbstractElnEntryUpdateCriteria(step.eln_entry.entry_type)
|
|
1300
|
-
|
|
1301
|
-
# These two variables could be iterables that aren't lists. Convert them to plain
|
|
1302
|
-
# lists, since that's what the update criteria is expecting.
|
|
1303
|
-
if related_entry_set is not None:
|
|
1304
|
-
related_entry_set = list(related_entry_set)
|
|
1305
|
-
if dependency_set is not None:
|
|
1306
|
-
dependency_set = list(dependency_set)
|
|
1307
|
-
|
|
1308
|
-
update.entry_name = entry_name
|
|
1309
|
-
update.related_entry_set = related_entry_set
|
|
1310
|
-
update.dependency_set = dependency_set
|
|
1311
|
-
update.entry_status = entry_status
|
|
1312
|
-
update.order = order
|
|
1313
|
-
update.description = description
|
|
1314
|
-
update.requires_grabber_plugin = requires_grabber_plugin
|
|
1315
|
-
update.is_initialization_required = is_initialization_required
|
|
1316
|
-
update.notebook_experiment_tab_id = notebook_experiment_tab_id
|
|
1317
|
-
update.entry_height = entry_height
|
|
1318
|
-
update.column_order = column_order
|
|
1319
|
-
update.column_span = column_span
|
|
1320
|
-
update.is_removable = is_removable
|
|
1321
|
-
update.is_renamable = is_renamable
|
|
1322
|
-
update.source_entry_id = source_entry_id
|
|
1323
|
-
update.clear_source_entry_id = clear_source_entry_id
|
|
1324
|
-
update.is_hidden = is_hidden
|
|
1325
|
-
update.is_static_View = is_static_view
|
|
1326
|
-
update.is_shown_in_template = is_shown_in_template
|
|
1327
|
-
update.template_item_fulfilled_timestamp = template_item_fulfilled_timestamp
|
|
1328
|
-
update.clear_template_item_fulfilled_timestamp = clear_template_item_fulfilled_timestamp
|
|
1329
|
-
update.entry_options_map = entry_options_map
|
|
1330
|
-
|
|
1331
|
-
return update
|
|
1332
|
-
|
|
1333
1266
|
@staticmethod
|
|
1334
1267
|
def _merge_updates(new_update: AbstractElnEntryUpdateCriteria, old_update: AbstractElnEntryUpdateCriteria) -> None:
|
|
1335
1268
|
"""
|
|
@@ -1347,10 +1280,10 @@ class ExperimentHandler:
|
|
|
1347
1280
|
entry: ExperimentEntry = step.eln_entry
|
|
1348
1281
|
if update.entry_name is not None:
|
|
1349
1282
|
# PR-46477 - Ensure that the previous name of the updated entry already existed in the cache.
|
|
1350
|
-
if entry.entry_name in self.
|
|
1351
|
-
self.
|
|
1283
|
+
if entry.entry_name in self._steps_by_name:
|
|
1284
|
+
self._steps_by_name.pop(entry.entry_name)
|
|
1352
1285
|
entry.entry_name = update.entry_name
|
|
1353
|
-
self.
|
|
1286
|
+
self._steps_by_name.update({update.entry_name: step})
|
|
1354
1287
|
if update.related_entry_set is not None:
|
|
1355
1288
|
entry.related_entry_id_set = update.related_entry_set
|
|
1356
1289
|
if update.dependency_set is not None:
|
|
@@ -1491,7 +1424,7 @@ class ExperimentHandler:
|
|
|
1491
1424
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1492
1425
|
:return: The map of options for the input step.
|
|
1493
1426
|
"""
|
|
1494
|
-
step = self.
|
|
1427
|
+
step: ElnEntryStep = self.get_step(step)
|
|
1495
1428
|
if step not in self._step_options:
|
|
1496
1429
|
self._step_options.update(ExperimentReportUtil.get_experiment_entry_options(self.user,
|
|
1497
1430
|
self.get_all_steps()))
|
|
@@ -1514,12 +1447,16 @@ class ExperimentHandler:
|
|
|
1514
1447
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1515
1448
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1516
1449
|
:param mapping: The new options and values to add to the existing step options, provided as some Mapping
|
|
1517
|
-
(e.g. a
|
|
1518
|
-
for that key.
|
|
1450
|
+
(e.g. a dictionary). If an option key already exists and is provided in the mapping, overwrites the existing
|
|
1451
|
+
value for that key.
|
|
1519
1452
|
"""
|
|
1453
|
+
# PR-47698: Convert the given step to an ElnEntryStep if it is not already one.
|
|
1454
|
+
step: ElnEntryStep = self.get_step(step)
|
|
1520
1455
|
options: dict[str, str] = self.get_step_options(step)
|
|
1521
1456
|
options.update(mapping)
|
|
1522
|
-
|
|
1457
|
+
update = AbstractElnEntryUpdateCriteria(step.eln_entry.entry_type)
|
|
1458
|
+
update.entry_options_map = options
|
|
1459
|
+
self.force_step_update(step, update)
|
|
1523
1460
|
|
|
1524
1461
|
def initialize_step(self, step: Step) -> None:
|
|
1525
1462
|
"""
|
|
@@ -1535,9 +1472,11 @@ class ExperimentHandler:
|
|
|
1535
1472
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1536
1473
|
"""
|
|
1537
1474
|
# Avoid unnecessary calls if the step is already initialized.
|
|
1538
|
-
step: ElnEntryStep = self.
|
|
1475
|
+
step: ElnEntryStep = self.get_step(step)
|
|
1539
1476
|
if step.eln_entry.template_item_fulfilled_timestamp is None:
|
|
1540
|
-
|
|
1477
|
+
update = AbstractElnEntryUpdateCriteria(step.eln_entry.entry_type)
|
|
1478
|
+
update.template_item_fulfilled_timestamp = TimeUtil.now_in_millis()
|
|
1479
|
+
self.force_step_update(step, update)
|
|
1541
1480
|
|
|
1542
1481
|
def uninitialize_step(self, step: Step) -> None:
|
|
1543
1482
|
"""
|
|
@@ -1553,9 +1492,11 @@ class ExperimentHandler:
|
|
|
1553
1492
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1554
1493
|
"""
|
|
1555
1494
|
# Avoid unnecessary calls if the step is already uninitialized.
|
|
1556
|
-
step: ElnEntryStep = self.
|
|
1495
|
+
step: ElnEntryStep = self.get_step(step)
|
|
1557
1496
|
if step.eln_entry.template_item_fulfilled_timestamp is not None:
|
|
1558
|
-
|
|
1497
|
+
update = AbstractElnEntryUpdateCriteria(step.eln_entry.entry_type)
|
|
1498
|
+
update.clear_template_item_fulfilled_timestamp = True
|
|
1499
|
+
self.force_step_update(step, update)
|
|
1559
1500
|
|
|
1560
1501
|
def complete_step(self, step: Step) -> None:
|
|
1561
1502
|
"""
|
|
@@ -1570,7 +1511,7 @@ class ExperimentHandler:
|
|
|
1570
1511
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1571
1512
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1572
1513
|
"""
|
|
1573
|
-
step = self.
|
|
1514
|
+
step: ElnEntryStep = self.get_step(step)
|
|
1574
1515
|
if step.eln_entry.entry_status not in self._ENTRY_COMPLETE_STATUSES:
|
|
1575
1516
|
step.complete_step()
|
|
1576
1517
|
step.eln_entry.entry_status = ExperimentEntryStatus.Completed
|
|
@@ -1588,7 +1529,7 @@ class ExperimentHandler:
|
|
|
1588
1529
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1589
1530
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1590
1531
|
"""
|
|
1591
|
-
step = self.
|
|
1532
|
+
step: ElnEntryStep = self.get_step(step)
|
|
1592
1533
|
if step.eln_entry.entry_status in self._ENTRY_LOCKED_STATUSES:
|
|
1593
1534
|
step.unlock_step()
|
|
1594
1535
|
step.eln_entry.entry_status = ExperimentEntryStatus.UnlockedChangesRequired
|
|
@@ -1610,9 +1551,11 @@ class ExperimentHandler:
|
|
|
1610
1551
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1611
1552
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1612
1553
|
"""
|
|
1613
|
-
step = self.
|
|
1554
|
+
step: ElnEntryStep = self.get_step(step)
|
|
1614
1555
|
if step.eln_entry.entry_status in self._ENTRY_LOCKED_STATUSES:
|
|
1615
|
-
|
|
1556
|
+
update = AbstractElnEntryUpdateCriteria(step.eln_entry.entry_type)
|
|
1557
|
+
update.entry_status = ExperimentEntryStatus.Disabled
|
|
1558
|
+
self.force_step_update(step, update)
|
|
1616
1559
|
|
|
1617
1560
|
def step_is_submitted(self, step: Step) -> bool:
|
|
1618
1561
|
"""
|
|
@@ -1627,7 +1570,7 @@ class ExperimentHandler:
|
|
|
1627
1570
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1628
1571
|
:return: True if the step's status is Completed or CompletedApproved. False otherwise.
|
|
1629
1572
|
"""
|
|
1630
|
-
return self.
|
|
1573
|
+
return self.get_step(step).eln_entry.entry_status in self._ENTRY_COMPLETE_STATUSES
|
|
1631
1574
|
|
|
1632
1575
|
def step_is_locked(self, step: Step) -> bool:
|
|
1633
1576
|
"""
|
|
@@ -1643,7 +1586,7 @@ class ExperimentHandler:
|
|
|
1643
1586
|
:return: True if the step's status is Completed, CompletedApproved, Disabled, LockedAwaitingApproval,
|
|
1644
1587
|
or LockedRejected. False otherwise.
|
|
1645
1588
|
"""
|
|
1646
|
-
return self.
|
|
1589
|
+
return self.get_step(step).eln_entry.entry_status in self._ENTRY_LOCKED_STATUSES
|
|
1647
1590
|
|
|
1648
1591
|
# FR-47464: Some functions that can help with entry placement.
|
|
1649
1592
|
def get_all_tabs(self) -> list[ElnExperimentTab]:
|
|
@@ -1656,6 +1599,7 @@ class ExperimentHandler:
|
|
|
1656
1599
|
if not self._queried_all_tabs:
|
|
1657
1600
|
self._tabs = self._eln_man.get_tabs_for_experiment(self._exp_id)
|
|
1658
1601
|
self._tabs.sort(key=lambda t: t.tab_order)
|
|
1602
|
+
self._tabs_by_id = {tab.tab_id: tab for tab in self._tabs}
|
|
1659
1603
|
self._tabs_by_name = {tab.tab_name: tab for tab in self._tabs}
|
|
1660
1604
|
return self._tabs
|
|
1661
1605
|
|
|
@@ -1689,21 +1633,34 @@ class ExperimentHandler:
|
|
|
1689
1633
|
self.add_tab_to_cache(tab)
|
|
1690
1634
|
return tab
|
|
1691
1635
|
|
|
1692
|
-
def get_tab(self,
|
|
1636
|
+
def get_tab(self, tab: str | int, exception_on_none: bool = True) -> ElnExperimentTab:
|
|
1693
1637
|
"""
|
|
1694
1638
|
Return the tab with the input name.
|
|
1695
1639
|
|
|
1696
1640
|
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1697
1641
|
list of tabs in the experiment and caches them.
|
|
1698
1642
|
|
|
1699
|
-
:param
|
|
1700
|
-
:
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1643
|
+
:param tab: The name or order of the tab to get. The order is 1-indexed.
|
|
1644
|
+
:param exception_on_none: If True, raises an exception if no tab with the given name exists.
|
|
1645
|
+
:return: The tab with the input name, or None if no such tab exists.
|
|
1646
|
+
"""
|
|
1647
|
+
if isinstance(tab, str):
|
|
1648
|
+
if tab not in self._tabs_by_name:
|
|
1649
|
+
self.get_all_tabs()
|
|
1650
|
+
eln_tab = self._tabs_by_name.get(tab)
|
|
1651
|
+
elif isinstance(tab, int):
|
|
1652
|
+
# The given integer is expected to be 1-indexed, but we read from the list with a 0-index.
|
|
1653
|
+
tab -= 1
|
|
1654
|
+
tabs = self.get_all_tabs()
|
|
1655
|
+
eln_tab = tabs[tab] if len(tabs) > tab else None
|
|
1656
|
+
else:
|
|
1657
|
+
raise SapioException(f"Tab must be a string or an integer, not {type(tab)}.")
|
|
1658
|
+
if eln_tab is None and exception_on_none:
|
|
1659
|
+
raise SapioException(f"No tab with the name\\order \"{tab}\" exists in this experiment.")
|
|
1660
|
+
return eln_tab
|
|
1705
1661
|
|
|
1706
|
-
def get_steps_in_tab(self, tab: Tab, data_type: DataTypeIdentifier | None = None)
|
|
1662
|
+
def get_steps_in_tab(self, tab: Tab, data_type: DataTypeIdentifier | None = None) \
|
|
1663
|
+
-> list[ElnEntryStep]:
|
|
1707
1664
|
"""
|
|
1708
1665
|
Get all the steps in the input tab sorted in order of appearance.
|
|
1709
1666
|
|
|
@@ -1713,7 +1670,8 @@ class ExperimentHandler:
|
|
|
1713
1670
|
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1714
1671
|
and caches them.
|
|
1715
1672
|
|
|
1716
|
-
:param tab: The tab
|
|
1673
|
+
:param tab: The tab to get the steps of. This can be the tab's order, name, or the tab object itself.
|
|
1674
|
+
The order is 1-indexed.
|
|
1717
1675
|
:param data_type: The data type to filter the steps by. If None, all steps are returned.
|
|
1718
1676
|
:return: A list of all the steps in the input tab sorted in order of appearance.
|
|
1719
1677
|
"""
|
|
@@ -1722,9 +1680,30 @@ class ExperimentHandler:
|
|
|
1722
1680
|
for step in self.get_all_steps(data_type):
|
|
1723
1681
|
if step.eln_entry.notebook_experiment_tab_id == tab.tab_id:
|
|
1724
1682
|
steps.append(step)
|
|
1725
|
-
steps.sort(key=lambda s: (s.eln_entry.order, s.eln_entry.column_order))
|
|
1726
1683
|
return steps
|
|
1727
1684
|
|
|
1685
|
+
def get_tab_for_step(self, step: Step) -> ElnExperimentTab:
|
|
1686
|
+
"""
|
|
1687
|
+
Get the tab that a particular step is located in.
|
|
1688
|
+
|
|
1689
|
+
If no tab functions have been called before and a tab is being searched for by name, queries for the
|
|
1690
|
+
list of tabs in the experiment and caches them.
|
|
1691
|
+
|
|
1692
|
+
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1693
|
+
and caches them.
|
|
1694
|
+
|
|
1695
|
+
:param step:
|
|
1696
|
+
The step to get the position of.
|
|
1697
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1698
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1699
|
+
:return: The tab that the input step is located in.
|
|
1700
|
+
"""
|
|
1701
|
+
step: ElnEntryStep = self.get_step(step)
|
|
1702
|
+
tab_id = step.eln_entry.notebook_experiment_tab_id
|
|
1703
|
+
if tab_id not in self._tabs_by_id:
|
|
1704
|
+
self.get_all_tabs()
|
|
1705
|
+
return self._tabs_by_id.get(tab_id)
|
|
1706
|
+
|
|
1728
1707
|
def get_next_entry_order_in_tab(self, tab: Tab) -> int:
|
|
1729
1708
|
"""
|
|
1730
1709
|
Get the next available order for a new entry in the input tab.
|
|
@@ -1735,419 +1714,81 @@ class ExperimentHandler:
|
|
|
1735
1714
|
If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
|
|
1736
1715
|
and caches them.
|
|
1737
1716
|
|
|
1738
|
-
:param tab: The tab
|
|
1717
|
+
:param tab: The tab to get the steps of. This can be the tab's order, name, or the tab object itself.
|
|
1718
|
+
The order is 1-indexed.
|
|
1739
1719
|
:return: The next available order for a new entry in the input tab.
|
|
1740
1720
|
"""
|
|
1741
1721
|
steps = self.get_steps_in_tab(tab)
|
|
1742
1722
|
return steps[-1].eln_entry.order + 1 if steps else 0
|
|
1743
1723
|
|
|
1744
|
-
# FR-
|
|
1745
|
-
def
|
|
1746
|
-
|
|
1747
|
-
|
|
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
|
|
1724
|
+
# FR-47530: Add functions for dealing with entry positioning.
|
|
1725
|
+
def step_to_position(self, step: Step) -> ElnEntryPosition:
|
|
1726
|
+
"""
|
|
1727
|
+
Get the position of the input step in the experiment.
|
|
2094
1728
|
|
|
2095
|
-
|
|
2096
|
-
|
|
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)
|
|
1729
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1730
|
+
list of steps in the experiment and caches them.
|
|
2102
1731
|
|
|
2103
|
-
|
|
2104
|
-
|
|
1732
|
+
:param step:
|
|
1733
|
+
The step to get the position of.
|
|
1734
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1735
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1736
|
+
:return: The position of the input step in the experiment.
|
|
1737
|
+
"""
|
|
1738
|
+
step: ElnEntryStep = self.get_step(step)
|
|
1739
|
+
entry: ExperimentEntry = step.eln_entry
|
|
1740
|
+
return ElnEntryPosition(entry.notebook_experiment_tab_id,
|
|
1741
|
+
entry.order,
|
|
1742
|
+
entry.column_span,
|
|
1743
|
+
entry.column_order)
|
|
2105
1744
|
|
|
2106
|
-
def
|
|
2107
|
-
-> list[AbstractVeloxFieldDefinition] | None:
|
|
1745
|
+
def step_at_position(self, position: ElnEntryPosition) -> Step | None:
|
|
2108
1746
|
"""
|
|
2109
|
-
|
|
1747
|
+
Get the step at the input position in the experiment.
|
|
1748
|
+
|
|
1749
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1750
|
+
list of steps in the experiment and caches them.
|
|
1751
|
+
|
|
1752
|
+
:param position: The position to get the step at.
|
|
1753
|
+
:return: The step at the input position in the experiment, or None if no step exists at that position.
|
|
2110
1754
|
"""
|
|
2111
|
-
if
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
if
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
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]
|
|
2134
|
-
|
|
2135
|
-
def __to_eln_step(self, step: Step) -> ElnEntryStep:
|
|
2136
|
-
"""
|
|
2137
|
-
Convert a variable that could be either a string or an ElnEntryStep to just an ElnEntryStep.
|
|
2138
|
-
This will query and cache the steps for the experiment if the input step is a name and the steps have not been
|
|
2139
|
-
cached before.
|
|
1755
|
+
if position.tab_id is None or position.order is None:
|
|
1756
|
+
raise SapioException("The provided position must at least have a tab ID and order.")
|
|
1757
|
+
for step in self.get_steps_in_tab(position.tab_id):
|
|
1758
|
+
entry: ExperimentEntry = step.eln_entry
|
|
1759
|
+
if entry.order != position.order:
|
|
1760
|
+
continue
|
|
1761
|
+
if position.column_span is not None and entry.column_span != position.column_span:
|
|
1762
|
+
continue
|
|
1763
|
+
if position.column_order is not None and entry.column_order != position.column_order:
|
|
1764
|
+
continue
|
|
1765
|
+
return step
|
|
1766
|
+
return None
|
|
2140
1767
|
|
|
2141
|
-
|
|
1768
|
+
# FR-47530: Create a function for adding protocol templates to the experiment.
|
|
1769
|
+
def add_protocol(self, protocol: ProtocolTemplateInfo | int, position: ElnEntryPosition) -> list[ElnEntryStep]:
|
|
2142
1770
|
"""
|
|
2143
|
-
|
|
1771
|
+
Add a protocol to the experiment. Updates the handler cache with the newly created entries.
|
|
1772
|
+
|
|
1773
|
+
:param protocol: The protocol to add. This can be either a ProtocolTemplateInfo object or the ID of the
|
|
1774
|
+
protocol template.
|
|
1775
|
+
:param position: The position that the protocol's first entry will be placed at.
|
|
1776
|
+
:return: The newly created protocol entries.
|
|
1777
|
+
"""
|
|
1778
|
+
protocol = protocol if isinstance(protocol, int) else protocol.template_id
|
|
1779
|
+
new_entries: list[ExperimentEntry] = self._eln_man.add_protocol_template(self._exp_id, protocol, position)
|
|
1780
|
+
return self.add_entries_to_caches(new_entries)
|
|
1781
|
+
|
|
1782
|
+
# CR-47700: Deleted __to_eln_step since it became redundant with the get_step method.
|
|
2144
1783
|
|
|
2145
1784
|
def __to_eln_tab(self, tab: Tab) -> ElnExperimentTab:
|
|
2146
1785
|
"""
|
|
2147
|
-
Convert a variable that could be either a
|
|
1786
|
+
Convert a variable that could be either a tab name, tab order, or ElnExperimentTab to just a tab object.
|
|
2148
1787
|
This will query and cache the tabs for the experiment if the input tab is a name and the tabs have not been
|
|
2149
1788
|
cached before.
|
|
2150
1789
|
|
|
2151
1790
|
:return: The input tab as an ElnExperimentTab.
|
|
2152
1791
|
"""
|
|
2153
|
-
|
|
1792
|
+
if not isinstance(tab, ElnExperimentTab):
|
|
1793
|
+
return self.get_tab(tab)
|
|
1794
|
+
return tab
|