sapiopycommons 2025.3.27a461__py3-none-any.whl → 2025.4.3a467__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.

@@ -26,6 +26,7 @@ from sapiopylib.rest.pojo.eln.SapioELNEnums import ExperimentEntryStatus, ElnExp
26
26
  ElnBaseDataType
27
27
  from sapiopylib.rest.pojo.eln.eln_headings import ElnExperimentTab, ElnExperimentTabAddCriteria
28
28
  from sapiopylib.rest.pojo.eln.field_set import ElnFieldSetInfo
29
+ from sapiopylib.rest.pojo.eln.protocol_template import ProtocolTemplateInfo
29
30
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
30
31
  from sapiopylib.rest.pojo.webhook.WebhookDirective import ElnExperimentDirective
31
32
  from sapiopylib.rest.pojo.webhook.WebhookResult import SapioWebhookResult
@@ -40,6 +41,7 @@ from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
40
41
  from sapiopylib.rest.utils.recordmodel.properties import Child
41
42
 
42
43
  from sapiopycommons.datatype.data_fields import SystemFields
44
+ from sapiopycommons.datatype.experiment_cache import ExperimentCacheManager
43
45
  from sapiopycommons.eln.experiment_report_util import ExperimentReportUtil
44
46
  from sapiopycommons.eln.experiment_tags import PLATE_DESIGNER_PLUGIN
45
47
  from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIdentifier, UserIdentifier, \
@@ -51,8 +53,8 @@ from sapiopycommons.recordmodel.record_handler import RecordHandler
51
53
  Step: TypeAlias = str | ElnEntryStep
52
54
  """An object representing an identifier to an ElnEntryStep. May be either the name of the step or the ElnEntryStep
53
55
  itself."""
54
- Tab: TypeAlias = str | ElnExperimentTab
55
- """An object representing an identifier to an ElnExperimentTab. May be either the name of the tab or the
56
+ 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
56
58
  ElnExperimentTab itself."""
57
59
  ElnDataTypeFields: TypeAlias = AbstractVeloxFieldDefinition | ElnFieldSetInfo | str | int
58
60
  """An object representing an identifier to an ElnDataType field. These can be field definitions for ad hoc fields,
@@ -77,6 +79,8 @@ class ExperimentHandler:
77
79
  # Managers.
78
80
  _eln_man: ElnManager
79
81
  """The ELN manager. Used for updating the experiment and its steps."""
82
+ _exp_cache: ExperimentCacheManager
83
+ """The experiment cache manager. Used for caching experiment-related information."""
80
84
  _inst_man: RecordModelInstanceManager
81
85
  """The record model instance manager. Used for wrapping the data records of a step as record models."""
82
86
  _rec_handler: RecordHandler
@@ -113,9 +117,6 @@ class ExperimentHandler:
113
117
  _tabs_by_name: dict[str, ElnExperimentTab]
114
118
  """The tabs for this experiment by their name. Only cached when first accessed."""
115
119
 
116
- _predefined_fields: dict[str, dict[str, AbstractVeloxFieldDefinition]]
117
- """A dictionary of predefined fields for each ELN data type. Only cached when first accessed."""
118
-
119
120
  # Constants
120
121
  _ENTRY_COMPLETE_STATUSES = [ExperimentEntryStatus.Completed, ExperimentEntryStatus.CompletedApproved]
121
122
  """The set of statuses that an ELN entry could have and be considered completed/submitted."""
@@ -173,6 +174,7 @@ class ExperimentHandler:
173
174
 
174
175
  # Grab various managers that may be used.
175
176
  self._eln_man = DataMgmtServer.get_eln_manager(self.user)
177
+ self._exp_cache = ExperimentCacheManager(self.user)
176
178
  self._inst_man = RecordModelManager(self.user).instance_manager
177
179
  self._rec_handler = RecordHandler(self.user)
178
180
 
@@ -284,30 +286,35 @@ class ExperimentHandler:
284
286
  self._tabs.clear()
285
287
  self._tabs_by_name.clear()
286
288
 
287
- def add_entry_to_caches(self, entry: ExperimentEntry | ElnEntryStep) -> None:
289
+ def add_entry_to_caches(self, entry: ExperimentEntry | ElnEntryStep) -> ElnEntryStep:
288
290
  """
289
291
  Add the given entry to the cache of steps for this experiment. This is necessary in order for certain methods to
290
292
  work. You should only need to do this if you have created a new entry in your code using a method outside
291
293
  of this ExperimentHandler.
292
294
 
293
295
  :param entry: The entry to add to the cache.
296
+ :return: The entry that was added to the cache as an ElnEntryStep.
294
297
  """
295
298
  if isinstance(entry, ExperimentEntry):
296
299
  entry = ElnEntryStep(self._protocol, entry)
297
300
  self._steps.update({entry.get_name(): entry})
298
301
  self._steps_by_id.update({entry.get_id(): entry})
299
302
  # Skipping the options cache. The get_step_options method will update the cache when necessary.
303
+ return entry
300
304
 
301
- def add_entries_to_caches(self, entries: list[ExperimentEntry | ElnEntryStep]) -> None:
305
+ def add_entries_to_caches(self, entries: list[ExperimentEntry | ElnEntryStep]) -> list[ElnEntryStep]:
302
306
  """
303
307
  Add the given entries to the cache of steps for this experiment. This is necessary in order for certain methods
304
308
  to work. You should only need to do this if you have created a new entry in your code using a method outside
305
309
  of this ExperimentHandler.
306
310
 
307
311
  :param entries: The entries to add to the cache.
312
+ :return: The entries that were added to the cache as ElnEntrySteps.
308
313
  """
314
+ new_entries: list[ElnEntryStep] = []
309
315
  for entry in entries:
310
- self.add_entry_to_caches(entry)
316
+ new_entries.append(self.add_entry_to_caches(entry))
317
+ return new_entries
311
318
 
312
319
  def add_tab_to_cache(self, tab: ElnExperimentTab) -> None:
313
320
  """
@@ -348,22 +355,10 @@ class ExperimentHandler:
348
355
  :param active_templates_only: Whether only active templates should be queried for.
349
356
  :return: The newly created experiment.
350
357
  """
351
- template_query = TemplateExperimentQueryPojo(latest_version_only=(template_version is None),
352
- active_templates_only=active_templates_only)
353
- templates: list[ElnTemplate] = context.eln_manager.get_template_experiment_list(template_query)
354
- launch_template: ElnTemplate | None = None
355
- for template in templates:
356
- if template.template_name != template_name:
357
- continue
358
- if template_version is not None and template.template_version != template_version:
359
- continue
360
- launch_template = template
361
- break
362
- if launch_template is None:
363
- raise SapioException(f"No template with the name \"{template_name}\"" +
364
- ("" if template_version is None else f" and the version {template_version}") +
365
- f" found.")
366
-
358
+ launch_template: ElnTemplate = ExperimentCacheManager(context).get_experiment_template(template_name,
359
+ active_templates_only,
360
+ template_version,
361
+ first_match=True)
367
362
  if experiment_name is None:
368
363
  experiment_name: str = launch_template.display_name
369
364
  if parent_record is not None:
@@ -787,8 +782,9 @@ class ExperimentHandler:
787
782
  return
788
783
  dt: str = AliasUtil.to_singular_data_type_name(records)
789
784
  if ElnBaseDataType.is_base_data_type(dt):
790
- raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. "
791
- f"Use add_eln_rows or add_sample_details instead.")
785
+ raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. ELN "
786
+ f"records that are committed to the system will automatically appear in the ELN entry "
787
+ f"with the matching data type name.")
792
788
  if dt != step.get_data_type_names()[0]:
793
789
  raise SapioException(f"Cannot add {dt} records to entry {step.get_name()} of type "
794
790
  f"{step.get_data_type_names()[0]}.")
@@ -814,8 +810,9 @@ class ExperimentHandler:
814
810
  return
815
811
  dt: str = AliasUtil.to_singular_data_type_name(records)
816
812
  if ElnBaseDataType.is_base_data_type(dt):
817
- raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. "
818
- f"Use remove_eln_rows or remove_sample_details instead.")
813
+ # CR-47532: Add remove_step_records support for Experiment Detail and Sample Detail entries.
814
+ self.remove_eln_rows(step, records)
815
+ return
819
816
  if dt != step.get_data_type_names()[0]:
820
817
  raise SapioException(f"Cannot remove {dt} records from entry {step.get_name()} of type "
821
818
  f"{step.get_data_type_names()[0]}.")
@@ -844,9 +841,14 @@ class ExperimentHandler:
844
841
  step = self.__to_eln_step(step)
845
842
  if records:
846
843
  dt: str = AliasUtil.to_singular_data_type_name(records)
844
+ # CR-47532: Add set_step_records support for Experiment Detail and Sample Detail entries.
847
845
  if ElnBaseDataType.is_base_data_type(dt):
848
- raise SapioException(f"{dt} is an ELN data type. This function call has no effect on ELN data types. "
849
- f"Use add_eln_rows or add_sample_details instead.")
846
+ remove_rows: list[PyRecordModel] = []
847
+ for record in self.get_step_models(step):
848
+ if record not in records:
849
+ remove_rows.append(record)
850
+ self.remove_eln_rows(step, remove_rows)
851
+ return
850
852
  if dt != step.get_data_type_names()[0]:
851
853
  raise SapioException(f"Cannot set {dt} records for entry {step.get_name()} of type "
852
854
  f"{step.get_data_type_names()[0]}.")
@@ -873,6 +875,23 @@ class ExperimentHandler:
873
875
  step.eln_entry.record_id = AliasUtil.to_data_record(record).record_id
874
876
 
875
877
  # FR-46496 - Provide functions for adding and removing rows from an ELN data type entry.
878
+ def add_eln_row(self, step: Step, wrapper_type: type[WrappedType] | None = None) -> WrappedType | PyRecordModel:
879
+ """
880
+ Add a row to an ELNExperimentDetail or ELNSampleDetail table entry. The row will not appear in the system
881
+ until a record manager store and commit has occurred.
882
+
883
+ If no step functions have been called before and a step is being searched for by name, queries for the
884
+ list of steps in the experiment and caches them.
885
+
886
+ :param step:
887
+ The step may be provided as either a string for the name of the step or an ElnEntryStep.
888
+ If given a name, throws an exception if no step of the given name exists in the experiment.
889
+ :param wrapper_type: Optionally wrap the ELN data type in a record model wrapper. If not provided, returns
890
+ an unwrapped PyRecordModel.
891
+ :return: The newly created row.
892
+ """
893
+ return self.add_eln_rows(step, 1, wrapper_type)[0]
894
+
876
895
  def add_eln_rows(self, step: Step, count: int, wrapper_type: type[WrappedType] | None = None) \
877
896
  -> list[WrappedType] | list[PyRecordModel]:
878
897
  """
@@ -901,10 +920,64 @@ class ExperimentHandler:
901
920
  return self._inst_man.wrap_list(records, wrapper_type)
902
921
  return records
903
922
 
904
- def add_eln_row(self, step: Step, wrapper_type: type[WrappedType] | None = None) -> WrappedType | PyRecordModel:
923
+ def add_sample_detail(self, step: Step, sample: RecordModel,
924
+ wrapper_type: type[WrappedType] | None = None) \
925
+ -> WrappedType | PyRecordModel:
905
926
  """
906
- Add a row to an ELNExperimentDetail or ELNSampleDetail table entry. The row will not appear in the system
907
- until a record manager store and commit has occurred.
927
+ Add a sample detail to a sample detail entry while relating it to the input sample record.
928
+
929
+ :param step:
930
+ The step may be provided as either a string for the name of the step or an ElnEntryStep.
931
+ If given a name, throws an exception if no step of the given name exists in the experiment.
932
+ :param sample: The sample record to add the sample detail to.
933
+ :param wrapper_type: Optionally wrap the sample detail in a record model wrapper. If not provided, returns
934
+ an unwrapped PyRecordModel.
935
+ :return: The newly created sample detail.
936
+ """
937
+ return self.add_sample_details(step, [sample], wrapper_type)[0]
938
+
939
+ def add_sample_details(self, step: Step, samples: list[RecordModel],
940
+ wrapper_type: type[WrappedType] | None = None) \
941
+ -> list[WrappedType] | list[PyRecordModel]:
942
+ """
943
+ Add sample details to a sample details entry while relating them to the input sample records.
944
+
945
+ :param step:
946
+ The step may be provided as either a string for the name of the step or an ElnEntryStep.
947
+ If given a name, throws an exception if no step of the given name exists in the experiment.
948
+ :param samples: The sample records to add the sample details to.
949
+ :param wrapper_type: Optionally wrap the sample details in a record model wrapper. If not provided, returns
950
+ an unwrapped PyRecordModel.
951
+ :return: The newly created sample details. The indices of the samples in the input list match the index of the
952
+ sample details in this list that they are related to.
953
+ """
954
+ step = self.__to_eln_step(step)
955
+ if step.eln_entry.entry_type != ElnEntryType.Table:
956
+ raise SapioException("The provided step is not a table entry.")
957
+ dt: str = step.get_data_type_names()[0]
958
+ if not ElnBaseDataType.is_eln_type(dt) or ElnBaseDataType.get_base_type(dt) != ElnBaseDataType.SAMPLE_DETAIL:
959
+ raise SapioException("The provided step is not an ELNSampleDetail entry.")
960
+ records: list[PyRecordModel] = []
961
+ for sample in samples:
962
+ if sample.data_type_name != "Sample":
963
+ raise SapioException(f"Received a {sample.data_type_name} record when Sample records were expected.")
964
+ detail: PyRecordModel = sample.add(Child.create_by_name(dt))
965
+ detail.set_field_values({
966
+ "SampleId": sample.get_field_value("SampleId"),
967
+ "OtherSampleId": sample.get_field_value("OtherSampleId")
968
+ })
969
+ records.append(detail)
970
+ if wrapper_type:
971
+ return self._inst_man.wrap_list(records, wrapper_type)
972
+ return records
973
+
974
+ def remove_eln_row(self, step: Step, record: SapioRecord) -> None:
975
+ """
976
+ Remove a row from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
977
+ records in the system that match the entry's data type. This means that removing rows from an ELN data type
978
+ table entry is equivalent to deleting the records for the rows.
979
+
980
+ The row will not be deleted in the system until a record manager store and commit has occurred.
908
981
 
909
982
  If no step functions have been called before and a step is being searched for by name, queries for the
910
983
  list of steps in the experiment and caches them.
@@ -912,11 +985,11 @@ class ExperimentHandler:
912
985
  :param step:
913
986
  The step may be provided as either a string for the name of the step or an ElnEntryStep.
914
987
  If given a name, throws an exception if no step of the given name exists in the experiment.
915
- :param wrapper_type: Optionally wrap the ELN data type in a record model wrapper. If not provided, returns
916
- an unwrapped PyRecordModel.
917
- :return: The newly created row.
988
+ :param record:
989
+ The record to remove from the given step.
990
+ The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
918
991
  """
919
- return self.add_eln_rows(step, 1, wrapper_type)[0]
992
+ self.remove_eln_rows(step, [record])
920
993
 
921
994
  def remove_eln_rows(self, step: Step, records: list[SapioRecord]) -> None:
922
995
  """
@@ -959,61 +1032,6 @@ class ExperimentHandler:
959
1032
  for record in record_models:
960
1033
  record.delete()
961
1034
 
962
- def remove_eln_row(self, step: Step, record: SapioRecord) -> None:
963
- """
964
- Remove a row from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
965
- records in the system that match the entry's data type. This means that removing rows from an ELN data type
966
- table entry is equivalent to deleting the records for the rows.
967
-
968
- The row will not be deleted in the system until a record manager store and commit has occurred.
969
-
970
- If no step functions have been called before and a step is being searched for by name, queries for the
971
- list of steps in the experiment and caches them.
972
-
973
- :param step:
974
- The step may be provided as either a string for the name of the step or an ElnEntryStep.
975
- If given a name, throws an exception if no step of the given name exists in the experiment.
976
- :param record:
977
- The record to remove from the given step.
978
- The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
979
- """
980
- self.remove_eln_rows(step, [record])
981
-
982
- def add_sample_details(self, step: Step, samples: list[RecordModel],
983
- wrapper_type: type[WrappedType] | None = None) \
984
- -> list[WrappedType] | list[PyRecordModel]:
985
- """
986
- Add sample details to a sample details entry while relating them to the input sample records.
987
-
988
- :param step:
989
- The step may be provided as either a string for the name of the step or an ElnEntryStep.
990
- If given a name, throws an exception if no step of the given name exists in the experiment.
991
- :param samples: The sample records to add the sample details to.
992
- :param wrapper_type: Optionally wrap the sample details in a record model wrapper. If not provided, returns
993
- an unwrapped PyRecordModel.
994
- :return: The newly created sample details. The indices of the samples in the input list match the index of the
995
- sample details in this list that they are related to.
996
- """
997
- step = self.__to_eln_step(step)
998
- if step.eln_entry.entry_type != ElnEntryType.Table:
999
- raise SapioException("The provided step is not a table entry.")
1000
- dt: str = step.get_data_type_names()[0]
1001
- if not ElnBaseDataType.is_eln_type(dt) or ElnBaseDataType.get_base_type(dt) != ElnBaseDataType.SAMPLE_DETAIL:
1002
- raise SapioException("The provided step is not an ELNSampleDetail entry.")
1003
- records: list[PyRecordModel] = []
1004
- for sample in samples:
1005
- if sample.data_type_name != "Sample":
1006
- raise SapioException(f"Received a {sample.data_type_name} record when Sample records were expected.")
1007
- detail: PyRecordModel = sample.add(Child.create_by_name(dt))
1008
- detail.set_field_values({
1009
- "SampleId": sample.get_field_value("SampleId"),
1010
- "OtherSampleId": sample.get_field_value("OtherSampleId")
1011
- })
1012
- records.append(detail)
1013
- if wrapper_type:
1014
- return self._inst_man.wrap_list(records, wrapper_type)
1015
- return records
1016
-
1017
1035
  # noinspection PyPep8Naming
1018
1036
  def update_step(self, step: Step,
1019
1037
  entry_name: str | None = None,
@@ -1708,7 +1726,7 @@ class ExperimentHandler:
1708
1726
  self.get_all_tabs()
1709
1727
  return self._tabs_by_name[tab_name]
1710
1728
 
1711
- def get_steps_in_tab(self, tab: Tab, data_type: DataTypeIdentifier | None = None) -> list[ElnEntryStep]:
1729
+ def get_steps_in_tab(self, tab: TabIdentifier, data_type: DataTypeIdentifier | None = None) -> list[ElnEntryStep]:
1712
1730
  """
1713
1731
  Get all the steps in the input tab sorted in order of appearance.
1714
1732
 
@@ -1718,7 +1736,8 @@ class ExperimentHandler:
1718
1736
  If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
1719
1737
  and caches them.
1720
1738
 
1721
- :param tab: The tab or tab name to get the steps of.
1739
+ :param tab: The tab to get the steps of. This can be either an ElnExperimentTab object, or the name or ID of
1740
+ the tab.
1722
1741
  :param data_type: The data type to filter the steps by. If None, all steps are returned.
1723
1742
  :return: A list of all the steps in the input tab sorted in order of appearance.
1724
1743
  """
@@ -1730,7 +1749,7 @@ class ExperimentHandler:
1730
1749
  steps.sort(key=lambda s: (s.eln_entry.order, s.eln_entry.column_order))
1731
1750
  return steps
1732
1751
 
1733
- def get_next_entry_order_in_tab(self, tab: Tab) -> int:
1752
+ def get_next_entry_order_in_tab(self, tab: TabIdentifier) -> int:
1734
1753
  """
1735
1754
  Get the next available order for a new entry in the input tab.
1736
1755
 
@@ -1740,12 +1759,71 @@ class ExperimentHandler:
1740
1759
  If the steps in the experiment have not been queried before, queries for the list of steps in the experiment
1741
1760
  and caches them.
1742
1761
 
1743
- :param tab: The tab or tab name to get the steps of.
1762
+ :param tab: The tab to get the next entry order of. This can be either an ElnExperimentTab object, or the name
1763
+ or ID of the tab.
1744
1764
  :return: The next available order for a new entry in the input tab.
1745
1765
  """
1746
1766
  steps = self.get_steps_in_tab(tab)
1747
1767
  return steps[-1].eln_entry.order + 1 if steps else 0
1748
1768
 
1769
+ # FR-47530: Add functions for dealing with entry positioning.
1770
+ def step_to_position(self, step: Step) -> ElnEntryPosition:
1771
+ """
1772
+ Get the position of the input step in the experiment.
1773
+
1774
+ If no step functions have been called before and a step is being searched for by name, queries for the
1775
+ list of steps in the experiment and caches them.
1776
+
1777
+ :param step:
1778
+ The step to get the position of.
1779
+ The step may be provided as either a string for the name of the step or an ElnEntryStep.
1780
+ If given a name, throws an exception if no step of the given name exists in the experiment.
1781
+ :return: The position of the input step in the experiment.
1782
+ """
1783
+ step: ElnEntryStep = self.__to_eln_step(step)
1784
+ entry: ExperimentEntry = step.eln_entry
1785
+ return ElnEntryPosition(entry.notebook_experiment_tab_id,
1786
+ entry.order,
1787
+ entry.column_span,
1788
+ entry.column_order)
1789
+
1790
+ def step_at_position(self, position: ElnEntryPosition) -> Step | None:
1791
+ """
1792
+ Get the step at the input position in the experiment.
1793
+
1794
+ If no step functions have been called before and a step is being searched for by name, queries for the
1795
+ list of steps in the experiment and caches them.
1796
+
1797
+ :param position: The position to get the step at.
1798
+ :return: The step at the input position in the experiment, or None if no step exists at that position.
1799
+ """
1800
+ if position.tab_id is None or position.order is None:
1801
+ raise SapioException("The provided position must at least have a tab ID and order.")
1802
+ for step in self.get_steps_in_tab(position.tab_id):
1803
+ entry: ExperimentEntry = step.eln_entry
1804
+ if entry.order != position.order:
1805
+ continue
1806
+ if position.column_span is not None and entry.column_span != position.column_span:
1807
+ continue
1808
+ if position.column_order is not None and entry.column_order != position.column_order:
1809
+ continue
1810
+ return step
1811
+ return None
1812
+
1813
+ # FR-47530: Create a function for adding protocol templates to the experiment.
1814
+ def add_protocol(self, protocol: ProtocolTemplateInfo | int, position: ElnEntryPosition) -> list[ElnEntryStep]:
1815
+ """
1816
+ Add a protocol to the experiment. Updates the handler cache with the newly created entries.
1817
+
1818
+ :param protocol: The protocol to add. This can be either a ProtocolTemplateInfo object or the ID of the
1819
+ protocol template.
1820
+ :param position: The position that the protocol's first entry will be placed at.
1821
+ :return: The newly created protocol entries.
1822
+ """
1823
+ protocol = protocol if isinstance(protocol, int) else protocol.template_id
1824
+ new_entries: list[ExperimentEntry] = self._eln_man.add_protocol_template(self._exp_id, protocol, position)
1825
+ return self.add_entries_to_caches(new_entries)
1826
+
1749
1827
  # FR-47468: Add functions for creating new entries in the experiment.
1750
1828
  def create_attachment_step(self, entry_name: str, data_type: DataTypeIdentifier,
1751
1829
  *,
@@ -2119,24 +2197,12 @@ class ExperimentHandler:
2119
2197
  for field in fields:
2120
2198
  if isinstance(field, AbstractVeloxFieldDefinition):
2121
2199
  field_defs.append(field)
2122
- elif isinstance(field, ElnFieldSetInfo):
2123
- field_defs.extend(self._eln_man.get_predefined_fields_from_field_set_id(field.field_set_id))
2200
+ elif isinstance(field, (int, ElnFieldSetInfo)):
2201
+ field_defs.extend(self._exp_cache.get_field_set_fields(field))
2124
2202
  elif isinstance(field, str):
2125
- field_defs.append(self._predefined_field(field, dt))
2126
- elif isinstance(field, int):
2127
- field_defs.extend(self._eln_man.get_predefined_fields_from_field_set_id(field))
2203
+ field_defs.append(self._exp_cache.get_predefined_field(field, dt))
2128
2204
  return field_defs
2129
2205
 
2130
- def _predefined_field(self, field_name: str, data_type: ElnBaseDataType) -> AbstractVeloxFieldDefinition:
2131
- """
2132
- Get the predefined field of the given name for the given ELN data type.
2133
- """
2134
- # TODO: Should this be in some sort of DataTypeCacheManager?
2135
- if data_type not in self._predefined_fields:
2136
- fields: list[AbstractVeloxFieldDefinition] = self._eln_man.get_predefined_fields(data_type)
2137
- self._predefined_fields[data_type.data_type_name] = {x.data_field_name: x for x in fields}
2138
- return self._predefined_fields[data_type.data_type_name][field_name]
2139
-
2140
2206
  def __to_eln_step(self, step: Step) -> ElnEntryStep:
2141
2207
  """
2142
2208
  Convert a variable that could be either a string or an ElnEntryStep to just an ElnEntryStep.
@@ -2147,7 +2213,7 @@ class ExperimentHandler:
2147
2213
  """
2148
2214
  return self.get_step(step) if isinstance(step, str) else step
2149
2215
 
2150
- def __to_eln_tab(self, tab: Tab) -> ElnExperimentTab:
2216
+ def __to_eln_tab(self, tab: TabIdentifier) -> ElnExperimentTab:
2151
2217
  """
2152
2218
  Convert a variable that could be either a string or an ElnExperimentTab to just an ElnExperimentTab.
2153
2219
  This will query and cache the tabs for the experiment if the input tab is a name and the tabs have not been
@@ -2155,4 +2221,8 @@ class ExperimentHandler:
2155
2221
 
2156
2222
  :return: The input tab as an ElnExperimentTab.
2157
2223
  """
2158
- return self.get_tab(tab) if isinstance(tab, str) else tab
2224
+ if isinstance(tab, str):
2225
+ return self.get_tab(tab)
2226
+ if isinstance(tab, int):
2227
+ return [x for x in self._tabs if x.tab_id == tab][0]
2228
+ return tab
@@ -327,10 +327,10 @@ class FileUtil:
327
327
  :param files: A dictionary of file name to file data as a string or bytes.
328
328
  :return: The bytes for a zip file containing the input files.
329
329
  """
330
- zip_buffer: io.BytesIO = io.BytesIO()
331
- with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
332
- for file_name, file_data in files.items():
333
- zip_file.writestr(file_name, file_data)
330
+ with io.BytesIO() as zip_buffer:
331
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
332
+ for file_name, file_data in files.items():
333
+ zip_file.writestr(file_name, file_data)
334
334
  return zip_buffer.getvalue()
335
335
 
336
336
  # Deprecated functions:
@@ -199,7 +199,7 @@ class AccessionRequestId(AbstractAccessionServiceOperator):
199
199
 
200
200
  Properties:
201
201
  numberOfCharacters: Number of characters maximum in the request ID.
202
- accessorName: This is a legacy variable from drum.getNextIdListByMapName(), which allows setting different "accessorName" from old system. We need this for compability patch for converting these to the new preference format.
202
+ accessorName: This is a legacy variable from drum.getNextIdListByMapName(), which allows setting different "accessorName" from old system. We need this for compatibility patch for converting these to the new preference format.
203
203
  """
204
204
  _num_of_characters: int
205
205
  _accessor_name: str
@@ -341,7 +341,7 @@ class AccessionService:
341
341
  def get_affixed_id_in_batch(self, data_type_name: str, data_field_name: str, num_ids: int, prefix: str | None,
342
342
  suffix: str | None, num_digits: int | None, start_num: int = 1) -> list[str]:
343
343
  """
344
- Get the batch affixed IDs that are maximal in cache and contiguious for a particular datatype.datafield under a given format.
344
+ Get the batch affixed IDs that are maximal in cache and contiguous for a particular datatype.datafield under a given format.
345
345
  :param data_type_name: The datatype name to look for max ID
346
346
  :param data_field_name: The datafield name to look for max ID
347
347
  :param num_ids: The number of IDs to accession.
@@ -0,0 +1,115 @@
1
+ from enum import Enum
2
+ from typing import Iterable, Any, Collection
3
+
4
+ from sapiopycommons.general.exceptions import SapioException
5
+
6
+
7
+ class ArrayTransformation(Enum):
8
+ """
9
+ An enumeration of the different transformations that can be applied to a 2D array.
10
+ """
11
+ ROTATE_CLOCKWISE = 0
12
+ ROTATE_COUNTER_CLOCKWISE = 1
13
+ ROTATE_180_DEGREES = 2
14
+ MIRROR_HORIZONTAL = 3
15
+ MIRROR_VERTICAL = 4
16
+
17
+
18
+ # FR-47524: Create a DataStructureUtils class that implements various collection utility functions from our Java
19
+ # libraries.
20
+ class DataStructureUtil:
21
+ """
22
+ Utility class for working with data structures. Copies from ListUtil, SetUtil, and various other classes in
23
+ our Java library.
24
+ """
25
+ @staticmethod
26
+ def find_first_or_none(values: Iterable[Any]) -> Any | None:
27
+ """
28
+ Get the first value from an iterable, or None if the iterable is empty.
29
+
30
+ :param values: An iterable of values.
31
+ :return: The first value from the input, or None if the input is empty.
32
+ """
33
+ return next(iter(values), None)
34
+
35
+ @staticmethod
36
+ def remove_null_values(values: Iterable[Any]) -> list[Any]:
37
+ """
38
+ Remove null values from a list.
39
+
40
+ :param values: An iterable of values.
41
+ :return: A list containing all the non-null values from the input.
42
+ """
43
+ return [value for value in values if value is not None]
44
+
45
+ @staticmethod
46
+ def transform_2d_array(values: Collection[Collection[Any]], transformation: ArrayTransformation) \
47
+ -> Collection[Collection[Any]]:
48
+ """
49
+ Perform a transformation on a 2D list.
50
+
51
+ :param values: An iterable of iterables. The iterables should all be of the same size.
52
+ :param transformation: The transformation to apply to the input.
53
+ :return: A new 2D list containing the input transformed according to the specified transformation.
54
+ """
55
+ x: int = len(values)
56
+ for row in values:
57
+ y = len(row)
58
+ if y != x:
59
+ raise SapioException(f"Input must be a square 2D array. The provided array has a length of {x} but "
60
+ f"at least one row has a length of {y}.")
61
+
62
+ match transformation:
63
+ case ArrayTransformation.ROTATE_CLOCKWISE:
64
+ return [list(row) for row in zip(*values[::-1])]
65
+ case ArrayTransformation.ROTATE_COUNTER_CLOCKWISE:
66
+ return [list(row) for row in zip(*values)][::-1]
67
+ case ArrayTransformation.ROTATE_180_DEGREES:
68
+ return [row[::-1] for row in values[::-1]]
69
+ case ArrayTransformation.MIRROR_HORIZONTAL:
70
+ return [list(row[::-1]) for row in values]
71
+ case ArrayTransformation.MIRROR_VERTICAL:
72
+ return values[::-1]
73
+
74
+ raise SapioException(f"Invalid transformation: {transformation}")
75
+
76
+ @staticmethod
77
+ def flatten_to_list(values: Iterable[Iterable[Any]]) -> list[Any]:
78
+ """
79
+ Flatten a list of lists into a single list.
80
+
81
+ :param values: An iterable of iterables.
82
+ :return: A single list containing all the values from the input. Elements are in the order they appear in the
83
+ input.
84
+ """
85
+ return [item for sublist in values for item in sublist]
86
+
87
+ @staticmethod
88
+ def flatten_to_set(values: Iterable[Iterable[Any]]) -> set[Any]:
89
+ """
90
+ Flatten a list of lists into a single set.
91
+
92
+ :param values: An iterable of iterables.
93
+ :return: A single set containing all the values from the input. Elements are in the order they appear in the
94
+ input.
95
+ """
96
+ return {item for subset in values for item in subset}
97
+
98
+ @staticmethod
99
+ def invert_dictionary(dictionary: dict[Any, Any], list_values: bool = False) \
100
+ -> dict[Any, Any] | dict[Any, list[Any]]:
101
+ """
102
+ Invert a dictionary, swapping keys and values. Note that the values of the input dictionary must be hashable.
103
+
104
+ :param dictionary: A dictionary to invert.
105
+ :param list_values: If false, keys that share the same value in the input dictionary will be overwritten in
106
+ the output dictionary so that only the last key remains. If true, the values of the output dictionary will
107
+ be lists where input keys that share the same value will be stored together.
108
+ :return: A new dictionary with the keys and values swapped.
109
+ """
110
+ if list_values:
111
+ inverted = {}
112
+ for key, value in dictionary.items():
113
+ inverted.setdefault(value, []).append(key)
114
+ return inverted
115
+ return {value: key for key, value in dictionary.items()}