sapiopycommons 2025.4.8a473__py3-none-any.whl → 2025.4.9a150__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.

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