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