sapiopycommons 2025.4.5a470__py3-none-any.whl → 2025.4.8a474__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.

@@ -5,60 +5,43 @@ 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, TabIdentifier
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, EntryAttachment, EntryRecordAttachment
24
+ ExperimentTempDataEntry
21
25
  from sapiopylib.rest.pojo.eln.ExperimentEntryCriteria import AbstractElnEntryUpdateCriteria, \
22
26
  ElnTableEntryUpdateCriteria, ElnFormEntryUpdateCriteria, ElnAttachmentEntryUpdateCriteria, \
23
27
  ElnPluginEntryUpdateCriteria, ElnDashboardEntryUpdateCriteria, ElnTextEntryUpdateCriteria, \
24
- ElnTempDataEntryUpdateCriteria, ElnEntryCriteria
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.field_set import ElnFieldSetInfo
29
32
  from sapiopylib.rest.pojo.eln.protocol_template import ProtocolTemplateInfo
30
33
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
31
34
  from sapiopylib.rest.pojo.webhook.WebhookDirective import ElnExperimentDirective
32
35
  from sapiopylib.rest.pojo.webhook.WebhookResult import SapioWebhookResult
33
36
  from sapiopylib.rest.utils.Protocols import ElnEntryStep, ElnExperimentProtocol
34
- from sapiopylib.rest.utils.plates.MultiLayerPlating import MultiLayerPlateConfig, MultiLayerPlateLayer, \
35
- MultiLayerDataTypeConfig, MultiLayerReplicateConfig, MultiLayerDilutionConfig
36
- from sapiopylib.rest.utils.plates.MultiLayerPlatingUtils import MultiLayerPlatingManager
37
- from sapiopylib.rest.utils.plates.PlatingUtils import PlatingOrder
38
37
  from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
39
38
  from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelInstanceManager, RecordModelManager
40
39
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
41
40
  from sapiopylib.rest.utils.recordmodel.properties import Child
42
41
 
43
- from sapiopycommons.datatype.data_fields import SystemFields
44
- from sapiopycommons.datatype.experiment_cache import ExperimentCacheManager
45
- from sapiopycommons.eln.experiment_report_util import ExperimentReportUtil
46
- from sapiopycommons.eln.experiment_tags import PLATE_DESIGNER_PLUGIN
47
- from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIdentifier, UserIdentifier, \
48
- DataTypeIdentifier, RecordModel, FieldMap, FieldIdentifier
49
- from sapiopycommons.general.exceptions import SapioException
50
- from sapiopycommons.general.time_util import TimeUtil
51
- from sapiopycommons.recordmodel.record_handler import RecordHandler
52
-
53
- Step: TypeAlias = str | ElnEntryStep
42
+ Step: TypeAlias = str | ExperimentEntryIdentifier
54
43
  """An object representing an identifier to an ElnEntryStep. May be either the name of the step or the ElnEntryStep
55
44
  itself."""
56
- TabIdentifier: TypeAlias = int | str | ElnExperimentTab
57
- """An object representing an identifier to an ElnExperimentTab. May be either the ID or name of the tab, or the
58
- ElnExperimentTab itself."""
59
- ElnDataTypeFields: TypeAlias = AbstractVeloxFieldDefinition | ElnFieldSetInfo | str | int
60
- """An object representing an identifier to an ElnDataType field. These can be field definitions for ad hoc fields,
61
- predefined field set info objects, predefined field names, or integers for predefined field set IDs."""
62
45
 
63
46
 
64
47
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
@@ -99,7 +82,9 @@ class ExperimentHandler:
99
82
 
100
83
  _queried_all_steps: bool
101
84
  """Whether this ExperimentHandler has queried the system for all steps in the experiment."""
102
- _steps: dict[str, ElnEntryStep]
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]
103
88
  """Steps from this experiment by their name. All steps are cached the first time any individual step is accessed."""
104
89
  _steps_by_id: dict[int, ElnEntryStep]
105
90
  """Steps from this experiment by their ID. All steps are cached the first time any individual step is accessed."""
@@ -113,7 +98,9 @@ class ExperimentHandler:
113
98
  _queried_all_tabs: bool
114
99
  """Whether this ExperimentHandler has queried the system for all tabs in the experiment."""
115
100
  _tabs: list[ElnExperimentTab]
116
- """The tabs for this experiment. Only cached when first accessed."""
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."""
117
104
  _tabs_by_name: dict[str, ElnExperimentTab]
118
105
  """The tabs for this experiment by their name. Only cached when first accessed."""
119
106
 
@@ -180,12 +167,14 @@ class ExperimentHandler:
180
167
 
181
168
  # Create empty caches to fill when necessary.
182
169
  self._queried_all_steps = False
183
- self._steps = {}
170
+ self._steps_by_name = {}
184
171
  self._steps_by_id = {}
172
+ self._steps = []
185
173
  self._step_options = {}
186
174
  self._step_updates = {}
187
175
 
188
176
  self._tabs = []
177
+ self._tabs_by_id = {}
189
178
  self._tabs_by_name = {}
190
179
 
191
180
  self._queried_all_tabs = False
@@ -201,7 +190,8 @@ class ExperimentHandler:
201
190
  for entry in self.context.experiment_entry_list:
202
191
  cache_steps.append(ElnEntryStep(self._protocol, entry))
203
192
  for step in cache_steps:
204
- self._steps.update({step.get_name(): step})
193
+ self._steps.append(step)
194
+ self._steps_by_name.update({step.get_name(): step})
205
195
  self._steps_by_id.update({step.get_id(): step})
206
196
 
207
197
  @staticmethod
@@ -266,6 +256,7 @@ class ExperimentHandler:
266
256
  """
267
257
  self._queried_all_steps = False
268
258
  self._steps.clear()
259
+ self._steps_by_name.clear()
269
260
  self._steps_by_id.clear()
270
261
  self._step_options.clear()
271
262
  self._step_updates.clear()
@@ -284,6 +275,7 @@ class ExperimentHandler:
284
275
  """
285
276
  self._queried_all_tabs = False
286
277
  self._tabs.clear()
278
+ self._tabs_by_id.clear()
287
279
  self._tabs_by_name.clear()
288
280
 
289
281
  def add_entry_to_caches(self, entry: ExperimentEntry | ElnEntryStep) -> ElnEntryStep:
@@ -297,7 +289,8 @@ class ExperimentHandler:
297
289
  """
298
290
  if isinstance(entry, ExperimentEntry):
299
291
  entry = ElnEntryStep(self._protocol, entry)
300
- self._steps.update({entry.get_name(): entry})
292
+ self._steps.append(entry)
293
+ self._steps_by_name.update({entry.get_name(): entry})
301
294
  self._steps_by_id.update({entry.get_id(): entry})
302
295
  # Skipping the options cache. The get_step_options method will update the cache when necessary.
303
296
  return entry
@@ -326,6 +319,7 @@ class ExperimentHandler:
326
319
  """
327
320
  self._tabs.append(tab)
328
321
  self._tabs.sort(key=lambda t: t.tab_order)
322
+ self._tabs_by_id[tab.tab_id] = tab
329
323
  self._tabs_by_name[tab.tab_name] = tab
330
324
 
331
325
  # FR-46495: Split the creation of the experiment in launch_experiment into a create_experiment function.
@@ -632,14 +626,14 @@ class ExperimentHandler:
632
626
  """
633
627
  return all([x is not None for x in self.get_steps(step_names, False)])
634
628
 
635
- def get_step(self, step_name: str, exception_on_none: bool = True) -> ElnEntryStep | None:
629
+ def get_step(self, step_name: str | int, exception_on_none: bool = True) -> ElnEntryStep | None:
636
630
  """
637
631
  Get the step of the given name from the experiment.
638
632
 
639
633
  If no step functions have been called before and a step is being searched for by name, queries for the
640
634
  list of steps in the experiment and caches them.
641
635
 
642
- :param step_name: The name for the step to return.
636
+ :param step_name: The name or ID for the step to return.
643
637
  :param exception_on_none: If false, returns None if the entry can't be found. If true, raises an exception
644
638
  when the named entry doesn't exist in the experiment.
645
639
  :return: An ElnEntrySteps matching the provided name. If there is no match and no exception is to be thrown,
@@ -647,31 +641,32 @@ class ExperimentHandler:
647
641
  """
648
642
  return self.get_steps([step_name], exception_on_none)[0]
649
643
 
650
- def get_steps(self, step_names: Iterable[str], exception_on_none: bool = True) -> list[ElnEntryStep | None]:
644
+ def get_steps(self, step_names: Iterable[str | int], exception_on_none: bool = True) -> list[ElnEntryStep | None]:
651
645
  """
652
646
  Get a list of steps of the given names from the experiment, sorted in the same order as the names are provided.
653
647
 
654
648
  If no step functions have been called before and a step is being searched for by name, queries for the
655
649
  list of steps in the experiment and caches them.
656
650
 
657
- :param step_names: A list of names for the entries to return and the order to return them in.
651
+ :param step_names: A list of names or IDs for the entries to return and the order to return them in.
658
652
  :param exception_on_none: If false, returns None for entries that can't be found. If true, raises an exception
659
653
  when the named entry doesn't exist in the experiment.
660
654
  :return: A list of ElnEntrySteps matching the provided names in the order they were provided in. If there is no
661
655
  match for a given step and no exception is to be thrown, returns None for that step.
662
656
  """
663
657
  ret_list: list[ElnEntryStep | None] = []
664
- for name in step_names:
658
+ for step_id in step_names:
665
659
  # If we haven't queried the system for all steps in the experiment yet, then the reason that a step is
666
660
  # missing may be because it wasn't in the webhook context. Therefore, query all steps and then check
667
661
  # if the step name is still missing from the experiment before potentially throwing an exception.
668
- if self._queried_all_steps is False and name not in self._steps:
669
- self._queried_all_steps = True
670
- self._steps.update({step.get_name(): step for step in self._protocol.get_sorted_step_list()})
671
-
672
- step: ElnEntryStep = self._steps.get(name)
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)
673
668
  if step is None and exception_on_none is True:
674
- raise SapioException(f"ElnEntryStep of name \"{name}\" not found in experiment with ID {self._exp_id}.")
669
+ raise SapioException(f"ElnEntryStep of name \"{step_id}\" not found in experiment with ID {self._exp_id}.")
675
670
  ret_list.append(step)
676
671
  return ret_list
677
672
 
@@ -686,14 +681,34 @@ class ExperimentHandler:
686
681
  :return: Every entry in the experiment in order of appearance that match the provided data type, if any.
687
682
  """
688
683
  if self._queried_all_steps is False:
689
- self._queried_all_steps = True
690
- self._steps.update({step.get_name(): step for step in self._protocol.get_sorted_step_list()})
691
- all_steps: list[ElnEntryStep] = self._protocol.get_sorted_step_list()
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
692
696
  if data_type is None:
693
697
  return all_steps
694
698
  data_type: str = AliasUtil.to_data_type_name(data_type)
695
699
  return [x for x in all_steps if data_type in x.get_data_type_names()]
696
700
 
701
+ def _query_all_steps(self) -> None:
702
+ """
703
+ Query the system for every step in the experiment and cache them.
704
+ """
705
+ self._queried_all_steps = True
706
+ self._protocol.invalidate()
707
+ self._steps = self._protocol.get_sorted_step_list()
708
+ for step in self._steps:
709
+ self._steps_by_name[step.get_name()] = step
710
+ self._steps_by_id[step.get_id()] = step
711
+
697
712
  def get_step_by_option(self, key: str, value: str | None = None) -> ElnEntryStep:
698
713
  """
699
714
  Retrieve the step in this experiment that contains an entry option with the provided key and value.
@@ -936,7 +951,7 @@ class ExperimentHandler:
936
951
  """
937
952
  return self.add_sample_details(step, [sample], wrapper_type)[0]
938
953
 
939
- def add_sample_details(self, step: Step, samples: list[RecordModel],
954
+ def add_sample_details(self, step: Step, samples: Iterable[RecordModel],
940
955
  wrapper_type: type[WrappedType] | None = None) \
941
956
  -> list[WrappedType] | list[PyRecordModel]:
942
957
  """
@@ -991,7 +1006,7 @@ class ExperimentHandler:
991
1006
  """
992
1007
  self.remove_eln_rows(step, [record])
993
1008
 
994
- def remove_eln_rows(self, step: Step, records: list[SapioRecord]) -> None:
1009
+ def remove_eln_rows(self, step: Step, records: Iterable[SapioRecord]) -> None:
995
1010
  """
996
1011
  Remove rows from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
997
1012
  records in the system that match the entry's data type. This means that removing rows from an ELN data type
@@ -1370,10 +1385,10 @@ class ExperimentHandler:
1370
1385
  entry: ExperimentEntry = step.eln_entry
1371
1386
  if update.entry_name is not None:
1372
1387
  # PR-46477 - Ensure that the previous name of the updated entry already existed in the cache.
1373
- if entry.entry_name in self._steps:
1374
- self._steps.pop(entry.entry_name)
1388
+ if entry.entry_name in self._steps_by_name:
1389
+ self._steps_by_name.pop(entry.entry_name)
1375
1390
  entry.entry_name = update.entry_name
1376
- self._steps.update({update.entry_name: step})
1391
+ self._steps_by_name.update({update.entry_name: step})
1377
1392
  if update.related_entry_set is not None:
1378
1393
  entry.related_entry_id_set = update.related_entry_set
1379
1394
  if update.dependency_set is not None:
@@ -1679,6 +1694,7 @@ class ExperimentHandler:
1679
1694
  if not self._queried_all_tabs:
1680
1695
  self._tabs = self._eln_man.get_tabs_for_experiment(self._exp_id)
1681
1696
  self._tabs.sort(key=lambda t: t.tab_order)
1697
+ self._tabs_by_id = {tab.tab_id: tab for tab in self._tabs}
1682
1698
  self._tabs_by_name = {tab.tab_name: tab for tab in self._tabs}
1683
1699
  return self._tabs
1684
1700
 
@@ -1712,21 +1728,29 @@ class ExperimentHandler:
1712
1728
  self.add_tab_to_cache(tab)
1713
1729
  return tab
1714
1730
 
1715
- def get_tab(self, tab_name: str) -> ElnExperimentTab:
1731
+ def get_tab(self, tab_name: str | int, exception_on_none: bool = True) -> ElnExperimentTab:
1716
1732
  """
1717
1733
  Return the tab with the input name.
1718
1734
 
1719
1735
  If no tab functions have been called before and a tab is being searched for by name, queries for the
1720
1736
  list of tabs in the experiment and caches them.
1721
1737
 
1722
- :param tab_name: The name of the tab to get.
1723
- :return: The tab with the input name.
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.
1724
1741
  """
1725
- if tab_name not in self._tabs_by_name:
1742
+ if tab_name not in self._tabs_by_name and tab_name not in self._tabs_by_id:
1726
1743
  self.get_all_tabs()
1727
- return self._tabs_by_name[tab_name]
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
1728
1751
 
1729
- def get_steps_in_tab(self, tab: TabIdentifier, data_type: DataTypeIdentifier | None = None) -> list[ElnEntryStep]:
1752
+ def get_steps_in_tab(self, tab: TabIdentifier | str, data_type: DataTypeIdentifier | None = None) \
1753
+ -> list[ElnEntryStep]:
1730
1754
  """
1731
1755
  Get all the steps in the input tab sorted in order of appearance.
1732
1756
 
@@ -1746,10 +1770,9 @@ class ExperimentHandler:
1746
1770
  for step in self.get_all_steps(data_type):
1747
1771
  if step.eln_entry.notebook_experiment_tab_id == tab.tab_id:
1748
1772
  steps.append(step)
1749
- steps.sort(key=lambda s: (s.eln_entry.order, s.eln_entry.column_order))
1750
1773
  return steps
1751
1774
 
1752
- def get_next_entry_order_in_tab(self, tab: TabIdentifier) -> int:
1775
+ def get_next_entry_order_in_tab(self, tab: TabIdentifier | str) -> int:
1753
1776
  """
1754
1777
  Get the next available order for a new entry in the input tab.
1755
1778
 
@@ -1824,385 +1847,6 @@ class ExperimentHandler:
1824
1847
  new_entries: list[ExperimentEntry] = self._eln_man.add_protocol_template(self._exp_id, protocol, position)
1825
1848
  return self.add_entries_to_caches(new_entries)
1826
1849
 
1827
- # FR-47468: Add functions for creating new entries in the experiment.
1828
- def create_attachment_step(self, entry_name: str, data_type: DataTypeIdentifier,
1829
- *,
1830
- position: ElnEntryPosition | None = None,
1831
- attachments: list[EntryAttachment] | list[SapioRecord] | None = None) -> ElnEntryStep:
1832
- """
1833
- Create a new attachment entry in the experiment.
1834
-
1835
- :param entry_name: The name of the entry.
1836
- :param data_type: The data type of the entry.
1837
- :param position: Information about where to place the entry in the experiment.
1838
- :param attachments: The list of attachments to initially populate the entry with.
1839
- :return: The newly created attachment entry.
1840
- """
1841
- step = self._create_step(ElnEntryType.Attachment, entry_name, data_type, position)
1842
- if attachments:
1843
- entry_attachments: list[EntryAttachment] = []
1844
- for entry in attachments:
1845
- if isinstance(entry, EntryAttachment):
1846
- entry_attachments.append(entry)
1847
- elif isinstance(entry, SapioRecord):
1848
- entry: SapioRecord
1849
- file_name: str = entry.get_field_value("FilePath")
1850
- if not file_name:
1851
- file_name = entry.get_field_value(SystemFields.DATA_RECORD_NAME__FIELD.field_name)
1852
- rec_id: int = AliasUtil.to_record_id(entry)
1853
- entry_attachments.append(EntryRecordAttachment(file_name, rec_id))
1854
- else:
1855
- raise SapioException("Attachments must be of type EntryAttachment or SapioRecord.")
1856
- update = ElnAttachmentEntryUpdateCriteria()
1857
- update.entry_attachment_list = attachments
1858
- self.force_step_update(step, update)
1859
- return step
1860
-
1861
- def create_form_step(self, entry_name: str,
1862
- data_type: DataTypeIdentifier,
1863
- fields: list[FieldIdentifier] | None = None,
1864
- layout_name: str | None = None,
1865
- form_names: list[str] | None = None,
1866
- *,
1867
- position: ElnEntryPosition | None = None,
1868
- record: SapioRecord | None = None) -> ElnEntryStep:
1869
- """
1870
- Create a new form entry in the experiment.
1871
-
1872
- :param entry_name: The name of the entry.
1873
- :param data_type: The data type of the entry.
1874
- :param fields: The list of data field names for the given data type that should appear on the
1875
- form. Fields will appear in the order they are provided. If not provided and a layout name is not provided,
1876
- the entry will be created with the data type's default layout.
1877
- :param layout_name: The name of the layout to use for the entry. If not provided and field names are not
1878
- provided, the entry will be created with the data type's default layout.
1879
- :param form_names: The list of layout component form names to use from the specified layout name. If not
1880
- provided, then all available forms from the specified layout will be used.
1881
- :param position: Information about where to place the entry in the experiment.
1882
- :param record: The record to initially populate the entry with.
1883
- :return: The newly created form entry.
1884
- """
1885
- if record:
1886
- rdt: str = AliasUtil.to_data_type_name(record)
1887
- sdt: str = AliasUtil.to_data_type_name(data_type)
1888
- if rdt != sdt:
1889
- raise SapioException(f"Cannot set {rdt} records for entry {entry_name} of type "
1890
- f"{sdt}.")
1891
-
1892
- step = self._create_step(ElnEntryType.Form, entry_name, data_type, position)
1893
- if fields or layout_name or form_names or record:
1894
- update = ElnFormEntryUpdateCriteria()
1895
- if fields:
1896
- update.data_field_name_list = AliasUtil.to_data_field_names(fields)
1897
- update.data_type_layout_name = layout_name
1898
- update.form_name_list = form_names
1899
- if record:
1900
- update.record_id = AliasUtil.to_record_id(record)
1901
- self.force_step_update(step, update)
1902
- return step
1903
-
1904
- def create_experiment_detail_form_step(self, entry_name: str,
1905
- fields: list[ElnDataTypeFields] | None = None,
1906
- field_map: FieldMap | None = None,
1907
- *,
1908
- position: ElnEntryPosition | None = None,
1909
- is_field_addable: bool | None = None,
1910
- is_existing_field_removable: bool | None = None) -> ElnEntryStep:
1911
- """
1912
- Create a new ELN experiment details form entry in the experiment.
1913
-
1914
- :param entry_name: The name of the entry.
1915
- :param fields: A list of objects representing the data fields that should appear on the entry. Fields
1916
- will appear in the order they are provided.
1917
- :param field_map: A field map that will be used to populate the entry. The data field names in
1918
- the map must match the field names of the provided field definitions.
1919
- :param position: Information about where to place the entry in the experiment.
1920
- :param is_field_addable: Whether users are able to add additional fields to this entry in the UI.
1921
- :param is_existing_field_removable: Whether users are able to remove existing fields from this entry in the UI.
1922
- :return: The newly created form entry.
1923
- """
1924
- return self._create_eln_dt_form_step(entry_name, ElnBaseDataType.EXPERIMENT_DETAIL, fields, field_map,
1925
- position=position,
1926
- is_field_addable=is_field_addable,
1927
- is_existing_field_removable=is_existing_field_removable)
1928
-
1929
- def create_sample_detail_form_step(self, entry_name: str,
1930
- fields: list[ElnDataTypeFields] | None = None,
1931
- field_map: FieldMap | None = None,
1932
- *,
1933
- position: ElnEntryPosition | None = None,
1934
- is_field_addable: bool | None = None,
1935
- is_existing_field_removable: bool | None = None) -> ElnEntryStep:
1936
- """
1937
- Create a new ELN sample details form entry in the experiment.
1938
-
1939
- :param entry_name: The name of the entry.
1940
- :param fields: A list of objects representing the data fields that should appear on the entry. Fields
1941
- will appear in the order they are provided.
1942
- :param field_map: A field map that will be used to populate the entry. The data field names in
1943
- the map must match the field names of the provided field definitions.
1944
- :param position: Information about where to place the entry in the experiment.
1945
- :param is_field_addable: Whether users are able to add additional fields to this entry in the UI.
1946
- :param is_existing_field_removable: Whether users are able to remove existing fields from this entry in the UI.
1947
- :return: The newly created form entry.
1948
- """
1949
- return self._create_eln_dt_form_step(entry_name, ElnBaseDataType.SAMPLE_DETAIL, fields, field_map,
1950
- position=position,
1951
- is_field_addable=is_field_addable,
1952
- is_existing_field_removable=is_existing_field_removable)
1953
-
1954
- def _create_eln_dt_form_step(self, entry_name: str,
1955
- dt: ElnBaseDataType,
1956
- fields: list[ElnDataTypeFields] | None = None,
1957
- field_map: FieldMap | None = None,
1958
- *,
1959
- position: ElnEntryPosition | None = None,
1960
- is_field_addable: bool | None = None,
1961
- is_existing_field_removable: bool | None = None) -> ElnEntryStep:
1962
- fields: list[AbstractVeloxFieldDefinition] | None = self._to_field_defs(fields, dt)
1963
- step = self._create_step(ElnEntryType.Form, entry_name, dt.data_type_name,
1964
- position,
1965
- field_definition_list=fields,
1966
- field_map_list=[field_map] if field_map else None)
1967
- if is_field_addable is not None or is_existing_field_removable is not None:
1968
- update = ElnFormEntryUpdateCriteria()
1969
- update.is_field_addable = is_field_addable
1970
- update.is_existing_field_removable = is_existing_field_removable
1971
- self.force_step_update(step, update)
1972
- return step
1973
-
1974
- def create_plugin_step(self, entry_name: str, data_type: DataTypeIdentifier, plugin_path: str, *,
1975
- position: ElnEntryPosition | None = None) -> ElnEntryStep:
1976
- """
1977
- Create a new plugin entry in the experiment.
1978
-
1979
- :param entry_name: The name of the entry.
1980
- :param data_type: The data type of the entry.
1981
- :param plugin_path: The plugin path to the CSP that determines this entry's functionality.
1982
- :param position: Information about where to place the entry in the experiment.
1983
- :return: The newly created plugin entry.
1984
- """
1985
- return self._create_step(ElnEntryType.Plugin, entry_name, data_type, position,
1986
- csp_plugin_name=plugin_path)
1987
-
1988
- def create_table_step(self, entry_name: str, data_type: DataTypeIdentifier,
1989
- fields: list[FieldIdentifier] | list[TableColumn] | None = None,
1990
- layout_name: str | None = None,
1991
- show_key_fields: bool | None = None,
1992
- *,
1993
- position: ElnEntryPosition | None = None,
1994
- records: list[SapioRecord] | None = None) -> ElnEntryStep:
1995
- """
1996
- Create a new table entry in the experiment.
1997
-
1998
- :param entry_name: The name of the entry.
1999
- :param data_type: The data type of the entry.
2000
- :param fields: The list of data field names for the given data type that should appear on the
2001
- table. You may also provide a list of TableColumns instead of a list of field names for when you want to
2002
- set the sort order and direction of the columns in the table. Fields will appear in the order they are
2003
- provided. If not provided and a layout name is not provided, the entry will be created with the data type's
2004
- default layout.
2005
- :param layout_name: The name of the layout to use for the entry. If not provided and field names are not
2006
- provided, the entry will be created with the data type's default layout.
2007
- :param show_key_fields: Whether the table should only show key fields of the data type.
2008
- :param position: Information about where to place the entry in the experiment.
2009
- :param records: The list of records to initially populate the entry with.
2010
- :return: The newly created table entry.
2011
- """
2012
- step = self._create_step(ElnEntryType.Table, entry_name, data_type, position)
2013
- if fields or layout_name:
2014
- update = ElnTableEntryUpdateCriteria()
2015
- if fields:
2016
- dt: str = AliasUtil.to_data_type_name(data_type)
2017
- columns: list[TableColumn] = []
2018
- for field in fields:
2019
- if isinstance(field, TableColumn):
2020
- columns.append(field)
2021
- else:
2022
- columns.append(TableColumn(dt, AliasUtil.to_data_field_name(field)))
2023
- update.table_column_list = columns
2024
- update.data_type_layout_name = layout_name
2025
- update.show_key_fields = show_key_fields
2026
- self.force_step_update(step, update)
2027
- if records:
2028
- self.set_step_records(step, records)
2029
- return step
2030
-
2031
- def create_experiment_detail_table_step(self, entry_name: str,
2032
- fields: list[ElnDataTypeFields] | None = None,
2033
- field_maps: list[FieldMap] | None = None, *,
2034
- position: ElnEntryPosition | None = None,
2035
- is_field_addable: bool | None = None,
2036
- is_existing_field_removable: bool | None = None) -> ElnEntryStep:
2037
- """
2038
- Create a new ELN experiment details table entry in the experiment.
2039
-
2040
- :param entry_name: The name of the entry.
2041
- :param fields: A list of objects representing the data fields that should appear on the entry. Fields
2042
- will appear in the order they are provided.
2043
- :param field_maps: A field maps list that will be used to populate the entry. The data field names in
2044
- the maps must match the field names of the provided field definitions.
2045
- :param position: Information about where to place the entry in the experiment.
2046
- :param is_field_addable: Whether users are able to add additional fields to this entry in the UI.
2047
- :param is_existing_field_removable: Whether users are able to remove existing fields from this entry in the UI.
2048
- :return: The newly created table entry.
2049
- """
2050
- return self._create_eln_dt_table_step(entry_name, ElnBaseDataType.EXPERIMENT_DETAIL, fields, field_maps,
2051
- position=position,
2052
- is_field_addable=is_field_addable,
2053
- is_existing_field_removable=is_existing_field_removable)
2054
-
2055
- def create_sample_detail_table_step(self, entry_name: str,
2056
- fields: list[ElnDataTypeFields] | None = None,
2057
- field_maps: list[FieldMap] | None = None, *,
2058
- position: ElnEntryPosition | None = None,
2059
- is_field_addable: bool | None = None,
2060
- is_existing_field_removable: bool | None = None) -> ElnEntryStep:
2061
- """
2062
- Create a new ELN sample details table entry in the experiment.
2063
-
2064
- :param entry_name: The name of the entry.
2065
- :param fields: A list of objects representing the data fields that should appear on the entry. Fields
2066
- will appear in the order they are provided.
2067
- :param field_maps: A field maps list that will be used to populate the entry. The data field names in
2068
- the maps must match the field names of the provided field definitions.
2069
- :param position: Information about where to place the entry in the experiment.
2070
- :param is_field_addable: Whether users are able to add additional fields to this entry in the UI.
2071
- :param is_existing_field_removable: Whether users are able to remove existing fields from this entry in the UI.
2072
- :return: The newly created table entry.
2073
- """
2074
- return self._create_eln_dt_table_step(entry_name, ElnBaseDataType.SAMPLE_DETAIL, fields, field_maps,
2075
- position=position,
2076
- is_field_addable=is_field_addable,
2077
- is_existing_field_removable=is_existing_field_removable)
2078
-
2079
- def _create_eln_dt_table_step(self, entry_name: str,
2080
- dt: ElnBaseDataType,
2081
- fields: list[ElnDataTypeFields] | None = None,
2082
- field_maps: list[FieldMap] | None = None, *,
2083
- position: ElnEntryPosition | None = None,
2084
- is_field_addable: bool | None = None,
2085
- is_existing_field_removable: bool | None = None) -> ElnEntryStep:
2086
- fields: list[AbstractVeloxFieldDefinition] | None = self._to_field_defs(fields, dt)
2087
- step = self._create_step(ElnEntryType.Table, entry_name, dt.data_type_name,
2088
- position,
2089
- field_definition_list=fields,
2090
- field_map_list=field_maps)
2091
- if is_field_addable is not None or is_existing_field_removable is not None:
2092
- update = ElnTableEntryUpdateCriteria()
2093
- update.is_field_addable = is_field_addable
2094
- update.is_existing_field_removable = is_existing_field_removable
2095
- self.force_step_update(step, update)
2096
- return step
2097
-
2098
- def create_temp_data_step(self, entry_name: str, data_type: DataTypeIdentifier, plugin_path: str, *,
2099
- position: ElnEntryPosition | None = None) -> ElnEntryStep:
2100
- """
2101
- Create a new temp data entry in the experiment.
2102
-
2103
- :param entry_name: The name of the entry.
2104
- :param data_type: The data type of the entry.
2105
- :param plugin_path: The plugin path to the plugin that populates the entry.
2106
- :param position: Information about where to place the entry in the experiment.
2107
- :return: The newly created temp data entry.
2108
- """
2109
- return self._create_step(ElnEntryType.TempData, entry_name, data_type, position,
2110
- temp_data_plugin_path=plugin_path)
2111
-
2112
- def create_text_step(self, entry_name: str, text: str | None = None, *,
2113
- position: ElnEntryPosition | None = None) -> ElnEntryStep:
2114
- """
2115
- Create a new text entry in the experiment.
2116
-
2117
- :param entry_name: The name of the entry.
2118
- :param text: The text to populate the entry with.
2119
- :param position: Information about where to place the entry in the experiment.
2120
- :return: The newly created text entry.
2121
- """
2122
- step: ElnEntryStep = self._create_step(ElnEntryType.Text, entry_name,
2123
- ElnBaseDataType.TEXT_ENTRY_DETAIL.data_type_name, position)
2124
- if text:
2125
- text_record: DataRecord = self.get_step_records(entry_name)[0]
2126
- text_record.set_field_value(ElnBaseDataType.get_text_entry_data_field_name(), text)
2127
- DataRecordManager(self.user).commit_data_records([text_record])
2128
- return step
2129
-
2130
- def create_plate_designer_step(self, entry_name: str, source_entry: Step, *,
2131
- position: ElnEntryPosition | None = None) -> ElnEntryStep:
2132
- """
2133
- Create a new 3D plate designer entry in the experiment.
2134
-
2135
- :param entry_name: The name of the entry.
2136
- :param source_entry: The entry that the plate designer will source its samples from.
2137
- :param position: Information about where to place the entry in the experiment.
2138
- :return: The newly created plate designer entry.
2139
- """
2140
- step = self.create_plugin_step(entry_name, "Sample", PLATE_DESIGNER_PLUGIN, position=position)
2141
- default_layer = MultiLayerPlateLayer(
2142
- MultiLayerDataTypeConfig("Sample"),
2143
- PlatingOrder.FillBy.BY_COLUMN,
2144
- MultiLayerReplicateConfig(),
2145
- MultiLayerDilutionConfig()
2146
- )
2147
- initial_step_options: dict[str, str] = {
2148
- "MultiLayerPlating_Plate_RecordIdList": "",
2149
- "MultiLayerPlating_Entry_Prefs": MultiLayerPlatingManager.get_entry_prefs_json([default_layer]),
2150
- "MultiLayerPlating_Entry_PrePlating_Prefs": MultiLayerPlatingManager.get_plate_configs_json(MultiLayerPlateConfig())
2151
- }
2152
- source_entry = self.__to_eln_step(source_entry)
2153
- self.force_step_update_params(step, entry_height=600, entry_options_map=initial_step_options,
2154
- related_entry_set=[source_entry.get_id()])
2155
- return step
2156
-
2157
- # TODO: Update these functions to use entry type-specific creation criteria once Sapiopylib supports that.
2158
- # This should take in an entry creation criteria object that handles all the abstract attributes that
2159
- # every entry type shares.
2160
- def _create_step(self, entry_type: ElnEntryType, entry_name: str, data_type: DataTypeIdentifier,
2161
- position: ElnEntryPosition | None = None, **kwargs) \
2162
- -> ElnEntryStep:
2163
- """
2164
- Create a new entry in the experiment of the given type.
2165
- """
2166
- if position is not None:
2167
- order: int = position.order
2168
- tab_id: int = position.tab_id
2169
- column_order: int = position.column_order
2170
- column_span: int = position.column_span
2171
- else:
2172
- last_tab: ElnExperimentTab = self.get_last_tab()
2173
- order: int = self.get_next_entry_order_in_tab(last_tab)
2174
- tab_id: int = last_tab.tab_id
2175
- column_order: int = 0
2176
- column_span: int = last_tab.max_number_of_columns
2177
-
2178
- data_type: str = AliasUtil.to_data_type_name(data_type)
2179
- crit = ElnEntryCriteria(entry_type, entry_name, data_type, order,
2180
- notebook_experiment_tab_id=tab_id,
2181
- column_order=column_order,
2182
- column_span=column_span,
2183
- **kwargs)
2184
- entry: ExperimentEntry = self._eln_man.add_experiment_entry(self._exp_id, crit)
2185
-
2186
- self.add_entry_to_caches(entry)
2187
- return ElnEntryStep(self._protocol, entry)
2188
-
2189
- def _to_field_defs(self, fields: list[ElnDataTypeFields], dt: ElnBaseDataType) \
2190
- -> list[AbstractVeloxFieldDefinition] | None:
2191
- """
2192
- Convert a list of ElnDataTypeField aliases to field definitions.
2193
- """
2194
- if not fields:
2195
- return None
2196
- field_defs: list[AbstractVeloxFieldDefinition] = []
2197
- for field in fields:
2198
- if isinstance(field, AbstractVeloxFieldDefinition):
2199
- field_defs.append(field)
2200
- elif isinstance(field, (int, ElnFieldSetInfo)):
2201
- field_defs.extend(self._exp_cache.get_field_set_fields(field))
2202
- elif isinstance(field, str):
2203
- field_defs.append(self._exp_cache.get_predefined_field(field, dt))
2204
- return field_defs
2205
-
2206
1850
  def __to_eln_step(self, step: Step) -> ElnEntryStep:
2207
1851
  """
2208
1852
  Convert a variable that could be either a string or an ElnEntryStep to just an ElnEntryStep.
@@ -2211,9 +1855,15 @@ class ExperimentHandler:
2211
1855
 
2212
1856
  :return: The input step as an ElnEntryStep.
2213
1857
  """
2214
- return self.get_step(step) if isinstance(step, str) else step
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
2215
1865
 
2216
- def __to_eln_tab(self, tab: TabIdentifier) -> ElnExperimentTab:
1866
+ def __to_eln_tab(self, tab: TabIdentifier | str) -> ElnExperimentTab:
2217
1867
  """
2218
1868
  Convert a variable that could be either a string or an ElnExperimentTab to just an ElnExperimentTab.
2219
1869
  This will query and cache the tabs for the experiment if the input tab is a name and the tabs have not been