sapiopycommons 2025.2.3a410__tar.gz → 2025.2.3a411__tar.gz

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 (81) hide show
  1. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/PKG-INFO +1 -1
  2. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/pyproject.toml +1 -1
  3. sapiopycommons-2025.2.3a411/src/sapiopycommons/elain/tool_of_tools.py +264 -0
  4. sapiopycommons-2025.2.3a411/tests/_do_not_add_init_py_here +0 -0
  5. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/.gitignore +0 -0
  6. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/LICENSE +0 -0
  7. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/README.md +0 -0
  8. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/__init__.py +0 -0
  9. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/callbacks/__init__.py +0 -0
  10. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/callbacks/callback_util.py +0 -0
  11. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/callbacks/field_builder.py +0 -0
  12. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/chem/IndigoMolecules.py +0 -0
  13. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/chem/Molecules.py +0 -0
  14. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/chem/__init__.py +0 -0
  15. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/customreport/__init__.py +0 -0
  16. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/customreport/auto_pagers.py +0 -0
  17. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/customreport/column_builder.py +0 -0
  18. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/customreport/custom_report_builder.py +0 -0
  19. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/customreport/term_builder.py +0 -0
  20. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/datatype/__init__.py +0 -0
  21. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/datatype/attachment_util.py +0 -0
  22. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/datatype/data_fields.py +0 -0
  23. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/datatype/pseudo_data_types.py +0 -0
  24. {sapiopycommons-2025.2.3a410/src/sapiopycommons/eln → sapiopycommons-2025.2.3a411/src/sapiopycommons/elain}/__init__.py +0 -0
  25. {sapiopycommons-2025.2.3a410/src/sapiopycommons/files → sapiopycommons-2025.2.3a411/src/sapiopycommons/eln}/__init__.py +0 -0
  26. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/eln/experiment_handler.py +0 -0
  27. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/eln/experiment_report_util.py +0 -0
  28. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/eln/plate_designer.py +0 -0
  29. {sapiopycommons-2025.2.3a410/src/sapiopycommons/general → sapiopycommons-2025.2.3a411/src/sapiopycommons/files}/__init__.py +0 -0
  30. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/files/complex_data_loader.py +0 -0
  31. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/files/file_bridge.py +0 -0
  32. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/files/file_bridge_handler.py +0 -0
  33. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/files/file_data_handler.py +0 -0
  34. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/files/file_util.py +0 -0
  35. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/files/file_validator.py +0 -0
  36. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/files/file_writer.py +0 -0
  37. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/flowcyto/flow_cyto.py +0 -0
  38. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/flowcyto/flowcyto_data.py +0 -0
  39. {sapiopycommons-2025.2.3a410/src/sapiopycommons/processtracking → sapiopycommons-2025.2.3a411/src/sapiopycommons/general}/__init__.py +0 -0
  40. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/general/accession_service.py +0 -0
  41. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/general/aliases.py +0 -0
  42. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/general/audit_log.py +0 -0
  43. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/general/custom_report_util.py +0 -0
  44. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/general/directive_util.py +0 -0
  45. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/general/exceptions.py +0 -0
  46. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/general/popup_util.py +0 -0
  47. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/general/sapio_links.py +0 -0
  48. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/general/storage_util.py +0 -0
  49. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/general/time_util.py +0 -0
  50. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/multimodal/multimodal.py +0 -0
  51. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/multimodal/multimodal_data.py +0 -0
  52. {sapiopycommons-2025.2.3a410/src/sapiopycommons/recordmodel → sapiopycommons-2025.2.3a411/src/sapiopycommons/processtracking}/__init__.py +0 -0
  53. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/processtracking/custom_workflow_handler.py +0 -0
  54. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/processtracking/endpoints.py +0 -0
  55. {sapiopycommons-2025.2.3a410/src/sapiopycommons/rules → sapiopycommons-2025.2.3a411/src/sapiopycommons/recordmodel}/__init__.py +0 -0
  56. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/recordmodel/record_handler.py +0 -0
  57. {sapiopycommons-2025.2.3a410/src/sapiopycommons/sftpconnect → sapiopycommons-2025.2.3a411/src/sapiopycommons/rules}/__init__.py +0 -0
  58. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/rules/eln_rule_handler.py +0 -0
  59. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/rules/on_save_rule_handler.py +0 -0
  60. {sapiopycommons-2025.2.3a410/src/sapiopycommons/webhook → sapiopycommons-2025.2.3a411/src/sapiopycommons/sftpconnect}/__init__.py +0 -0
  61. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/sftpconnect/sftp_builder.py +0 -0
  62. /sapiopycommons-2025.2.3a410/tests/_do_not_add_init_py_here → /sapiopycommons-2025.2.3a411/src/sapiopycommons/webhook/__init__.py +0 -0
  63. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/webhook/webhook_context.py +0 -0
  64. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/webhook/webhook_handlers.py +0 -0
  65. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/src/sapiopycommons/webhook/webservice_handlers.py +0 -0
  66. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/AF-A0A009IHW8-F1-model_v4.cif +0 -0
  67. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/accession_test.py +0 -0
  68. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/bio_reg_test.py +0 -0
  69. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/chem_test.py +0 -0
  70. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/chem_test_curation_queue.py +0 -0
  71. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/curation_queue_test.sdf +0 -0
  72. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/data_type_models.py +0 -0
  73. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/flowcyto/101_DEN084Y5_15_E01_008_clean.fcs +0 -0
  74. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/flowcyto/101_DEN084Y5_15_E03_009_clean.fcs +0 -0
  75. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/flowcyto/101_DEN084Y5_15_E05_010_clean.fcs +0 -0
  76. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/flowcyto/8_color_ICS.wsp +0 -0
  77. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/flowcyto/COVID19_W_001_O.fcs +0 -0
  78. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/flowcyto_test.py +0 -0
  79. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/kappa.chains.fasta +0 -0
  80. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/mafft_test.py +0 -0
  81. {sapiopycommons-2025.2.3a410 → sapiopycommons-2025.2.3a411}/tests/test.gb +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2025.2.3a410
3
+ Version: 2025.2.3a411
4
4
  Summary: Official Sapio Python API Utilities Package
5
5
  Project-URL: Homepage, https://github.com/sapiosciences
6
6
  Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sapiopycommons"
7
- version='2025.02.03a410'
7
+ version='2025.02.03a411'
8
8
  authors = [
9
9
  { name="Jonathan Steck", email="jsteck@sapiosciences.com" },
10
10
  { name="Yechen Qiao", email="yqiao@sapiosciences.com" },
@@ -0,0 +1,264 @@
1
+ import base64
2
+
3
+ from sapiopylib.rest.DataRecordManagerService import DataRecordManager
4
+ from sapiopylib.rest.ELNService import ElnManager
5
+ from sapiopylib.rest.User import SapioUser
6
+ from sapiopylib.rest.pojo.DataRecord import DataRecord
7
+ from sapiopylib.rest.pojo.chartdata.DashboardDefinition import GaugeChartDefinition
8
+ from sapiopylib.rest.pojo.chartdata.DashboardEnums import ChartGroupingType, ChartOperationType, ChartType
9
+ from sapiopylib.rest.pojo.chartdata.DashboardSeries import GaugeChartSeries
10
+ from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
11
+ from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry
12
+ from sapiopylib.rest.pojo.eln.ExperimentEntryCriteria import ElnEntryCriteria, ElnFormEntryUpdateCriteria
13
+ from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnEntryType, ElnBaseDataType
14
+ from sapiopylib.rest.pojo.eln.eln_headings import ElnExperimentTabAddCriteria, ElnExperimentTab
15
+ from sapiopylib.rest.pojo.eln.field_set import ElnFieldSetInfo
16
+ from sapiopylib.rest.utils.ProtocolUtils import ELNStepFactory
17
+ from sapiopylib.rest.utils.Protocols import ElnEntryStep, ElnExperimentProtocol
18
+
19
+ from sapiopycommons.general.exceptions import SapioException
20
+
21
+
22
+ # FR-47422: Create utility methods to assist the tool of tools.
23
+ def create_tot_headers(url: str, username: str, password: str, experiment_id: int, tab_prefix: str) \
24
+ -> tuple[str, dict[str, str]]:
25
+ """
26
+ Create the headers to be passed to a tool of tools endpoint.
27
+
28
+ :param url: The webservice URL of the system to make the changes in.
29
+ :param username: The username of the user making the changes.
30
+ :param password: The password of the user making the changes.
31
+ :param experiment_id: The ID of the experiment to make the changes in.
32
+ :param tab_prefix: The prefix to use for the tab name that will be created by the tool.
33
+ :return: The encoded credentials and the headers to be passed to the endpoint.
34
+ """
35
+ # Combine the credentials into the format "username:password"
36
+ credentials: str = f"{username}:{password}"
37
+ # Encode the credentials to bytes, then encode them using base64,
38
+ # and finally convert the result back into a string.
39
+ encoded_credentials: str = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
40
+ headers: dict[str, str] = {
41
+ "SAPIO_APP_API_KEY": f"Basic {encoded_credentials}",
42
+ "SAPIO_APP_API_URL": url,
43
+ "EXPERIMENT_ID": str(experiment_id),
44
+ "TAB_PREFIX": tab_prefix
45
+ }
46
+ return encoded_credentials, headers
47
+
48
+
49
+ def create_user_from_tot_headers(headers: dict[str, str]) -> SapioUser:
50
+ """
51
+ Create a SapioUser object from the headers passed to a tool of tools endpoint.
52
+
53
+ :param headers: The headers that were passed to the endpoint.
54
+ :return: A SapioUser object created from the headers that can be used to communicate with the Sapio server.
55
+ """
56
+ credentials = base64.b64decode(headers["SAPIO_APP_API_KEY"].removeprefix("Basic ")).decode("utf-8").split(":", 1)
57
+ return SapioUser(headers["SAPIO_APP_API_URL"], username=credentials[0], password=credentials[1])
58
+
59
+
60
+ class ToolOfToolsHelper:
61
+ """
62
+ A class with helper methods utilized by the Tool of Tools for the creation and updating of experiment tabs that
63
+ track a tool's progress and results.
64
+ """
65
+ # Contextual info.
66
+ user: SapioUser
67
+ tab_prefix: str
68
+ exp_id: int
69
+ _protocol: ElnExperimentProtocol
70
+
71
+ # Tool info.
72
+ name: str
73
+ description: str
74
+ results_data_type: str | None
75
+
76
+ # Managers.
77
+ eln_man: ElnManager
78
+ dr_man: DataRecordManager
79
+
80
+ # Stuff created by this helper.
81
+ _initialized: bool
82
+ """Whether a tab for this tool has been initialized."""
83
+ tab: ElnExperimentTab
84
+ """The tab that contains the tool's entries."""
85
+ description_entry: ElnEntryStep
86
+ """The text entry that displays the description of the tool."""
87
+ progress_entry: ElnEntryStep
88
+ """A hidden entry for tracking the progress of the tool."""
89
+ progress_record: DataRecord
90
+ """The record that stores the progress of the tool."""
91
+ progress_gauge_entry: ElnEntryStep
92
+ """A chart entry that displays the progress of the tool using the hidden progress entry."""
93
+ results_entry: ElnEntryStep | None
94
+ """An entry for displaying the results of the tool. If None, the tool does not produce result records."""
95
+
96
+ def __init__(self, headers: dict[str, str], name: str, description: str, results_data_type: str | None = None):
97
+ """
98
+ :param headers: The headers that were passed to the endpoint.
99
+ :param name: The name of the tool.
100
+ :param description: A description of the tool.
101
+ :param results_data_type: The data type name for the results of the tool. If None, the tool does not produce
102
+ result records.
103
+ """
104
+ self.user = create_user_from_tot_headers(headers)
105
+ self.exp_id = int(headers["EXPERIMENT_ID"])
106
+ self.tab_prefix = headers["TAB_PREFIX"]
107
+ # The experiment name and record ID aren't necessary to know.
108
+ self._protocol = ElnExperimentProtocol(ElnExperiment(self.exp_id, "", 0), self.user)
109
+
110
+ self.name = name
111
+ self.description = description
112
+ self.results_data_type = results_data_type
113
+
114
+ self.eln_man = ElnManager(self.user)
115
+ self.dr_man = DataRecordManager(self.user)
116
+
117
+ self._initialized = False
118
+
119
+ def initialize_tab(self) -> ElnExperimentTab:
120
+ if self._initialized:
121
+ return self.tab
122
+ self._initialized = True
123
+
124
+ # Create the tab for the tool progress and results.
125
+ # The entry IDs list can't be empty, so we need to create a dummy entry just to get the tab created.
126
+ tab_crit = ElnExperimentTabAddCriteria(f"{self.tab_prefix} {self.name}", [])
127
+ tab: ElnExperimentTab = self.eln_man.add_tab_for_experiment(self.exp_id, tab_crit)
128
+ self.tab = tab
129
+
130
+ # Create a hidden entry for tracking the progress of the tool.
131
+ field_sets: list[ElnFieldSetInfo] = self.eln_man.get_field_set_info_list()
132
+ progress_field_set: list[ElnFieldSetInfo] = [x for x in field_sets if
133
+ x.field_set_name == "Tool of Tools Progress"]
134
+ if not progress_field_set:
135
+ raise SapioException("Unable to locate the field set for the Tool of Tools progress.")
136
+ progress_entry_crit = ElnEntryCriteria(ElnEntryType.Form, f"ELaiN: {self.name} Progress",
137
+ ElnBaseDataType.EXPERIMENT_DETAIL.data_type_name, 1,
138
+ notebook_experiment_tab_id=tab.tab_id,
139
+ enb_field_set_id=progress_field_set[0].field_set_id)
140
+ progress_entry = ElnEntryStep(self._protocol,
141
+ self.eln_man.add_experiment_entry(self.exp_id, progress_entry_crit))
142
+ self.progress_entry = progress_entry
143
+ self.progress_record = progress_entry.get_records()[0]
144
+
145
+ # Hide the progress entry.
146
+ update_crit = ElnFormEntryUpdateCriteria()
147
+ update_crit.is_hidden = True
148
+ self.eln_man.update_experiment_entry(self.exp_id, progress_entry.get_id(), update_crit)
149
+
150
+ # Create a gauge entry to display the progress.
151
+ gauge_entry: ElnEntryStep = self._create_gauge_chart(self._protocol, progress_entry,
152
+ f"{self.name} Progress", "Progress")
153
+ self.progress_gauge_entry = gauge_entry
154
+
155
+ # Create the text entry that displays the description of the tool.
156
+ text_entry: ElnEntryStep = ELNStepFactory.create_text_entry(self._protocol, self.description)
157
+ self.description_entry = text_entry
158
+
159
+ # Create a results entry if this tool produces result records.
160
+ if self.results_data_type:
161
+ results_entry = ELNStepFactory.create_table_step(self._protocol, f"{self.name} Results", self.results_data_type)
162
+ self.results_entry = results_entry
163
+ else:
164
+ self.results_entry = None
165
+
166
+ return tab
167
+
168
+ def update_progress(self, progress: float, status_msg: str | None = None) -> None:
169
+ """
170
+ Updates the progress of the tool.
171
+
172
+ :param progress: A value between 0 and 100 representing the progress of the tool.
173
+ :param status_msg: A status message to display to the user alongside the progress gauge.
174
+ """
175
+ if not self._initialized:
176
+ raise SapioException("The tab for this tool has not been initialized.")
177
+ self.progress_record.set_field_value("Progress", progress)
178
+ self.progress_record.set_field_value("StatusMsg", status_msg)
179
+ self.dr_man.commit_data_records([self.progress_record])
180
+
181
+ def add_attachment_entry(self, file_name: str, file_data: str | bytes, entry_name: str,
182
+ tab: ElnExperimentTab | None = None) -> ExperimentEntry:
183
+ """
184
+ Add a new attachment entry to the experiment with the provided attachment data.
185
+
186
+ :param file_name: The name of the attachment.
187
+ :param file_data: The data of the attachment. This can be a string or bytes.
188
+ :param entry_name: Name of the attachment entry to create in the experiment.
189
+ :param tab: The tab where the attachment will be added. If not provided, the tab initialized by this helper
190
+ will be used.
191
+ :return: The created entry object.
192
+ """
193
+ # Check if the tab has been initialized or a tab has been provided.
194
+ if not self._initialized and tab is None:
195
+ raise SapioException("The tab for this tool has not been initialized. Either initialize a tab for this "
196
+ "tool or provide the tab to this function to add the attachment entry to.")
197
+ tab_id: int = self.tab.tab_id if tab is None else tab.tab_id
198
+
199
+ # Encode the file contents in base64.
200
+ if isinstance(file_data, str):
201
+ file_data: bytes = file_data.encode("utf-8")
202
+ base64_encoded: str = base64.b64encode(file_data).decode("utf-8")
203
+
204
+ # Crete an attachment entry with the provided data.
205
+ attachment_entry = self.eln_man.add_experiment_entry(
206
+ self.exp_id,
207
+ ElnEntryCriteria(ElnEntryType.Attachment, entry_name, "Attachment", order=2,
208
+ notebook_experiment_tab_id=tab_id, attachment_file_name=file_name,
209
+ attachment_data_base64=base64_encoded)
210
+ )
211
+
212
+ # Return the entry object for further use.
213
+ return attachment_entry
214
+
215
+ def add_attachment_entry_from_file_system(self, file_path: str, entry_name: str,
216
+ tab: ElnExperimentTab | None = None) -> ExperimentEntry:
217
+ """
218
+ Add a new attachment entry to the experiment with the provided file path to a file in the file system.
219
+
220
+ :param file_path: The path to a file in the system to attach to the experiment.
221
+ :param entry_name: Name of the attachment entry to create in the experiment.
222
+ :param tab: The tab where the attachment will be added. If not provided, the tab initialized by this helper
223
+ will be used.
224
+ :return: The created entry object.
225
+ """
226
+ # Check if the tab has been initialized or a tab has been provided.
227
+ # This is redundant with the same check in the add_attachment_entry function, but it's duplicated here as to
228
+ # not read the provided file and then find out we can't do anything with it anyway.
229
+ if not self._initialized and tab is None:
230
+ raise SapioException("The tab for this tool has not been initialized. Either initialize a tab for this "
231
+ "tool or provide the tab to this function to add the attachment entry to.")
232
+
233
+ with open(file_path, 'rb') as f:
234
+ file_contents: bytes = f.read()
235
+ return self.add_attachment_entry(file_path, file_contents, entry_name, tab)
236
+
237
+ # TODO: Remove this once pylib has a gauge chart function in ElnStepFactory.
238
+ @staticmethod
239
+ def _create_gauge_chart(protocol: ElnExperimentProtocol, data_source_step: ElnEntryStep, step_name: str,
240
+ field_name: str, group_by_field_name: str = "DataRecordName") -> ElnEntryStep:
241
+ """
242
+ Create a gauge chart step in the experiment protocol.
243
+ """
244
+ if not data_source_step.get_data_type_names():
245
+ raise ValueError("The data source step did not declare a data type name.")
246
+ data_type_name: str = data_source_step.get_data_type_names()[0]
247
+ series = GaugeChartSeries(data_type_name, field_name)
248
+ series.operation_type = ChartOperationType.VALUE
249
+ chart = _FixedGaugeChartDefinition()
250
+ chart.minimum_value = 0.
251
+ chart.maximum_value = 100.
252
+ chart.series_list = [series]
253
+ chart.grouping_type = ChartGroupingType.GROUP_BY_FIELD
254
+ chart.grouping_type_data_type_name = data_type_name
255
+ chart.grouping_type_data_field_name = group_by_field_name
256
+ dashboard, step = ELNStepFactory._create_dashboard_step_from_chart(chart, data_source_step, protocol, step_name)
257
+ protocol.invalidate()
258
+ return step
259
+
260
+
261
+ # TODO: This is only here because the get_chart_type function in pylib is wrong. Remove this once pylib is fixed.
262
+ class _FixedGaugeChartDefinition(GaugeChartDefinition):
263
+ def get_chart_type(self) -> ChartType:
264
+ return ChartType.GAUGE_CHART