sapiopycommons 2025.2.3a411__py3-none-any.whl → 2025.2.5a413__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.
- sapiopycommons/elain/tool_of_tools.py +99 -22
- {sapiopycommons-2025.2.3a411.dist-info → sapiopycommons-2025.2.5a413.dist-info}/METADATA +1 -1
- {sapiopycommons-2025.2.3a411.dist-info → sapiopycommons-2025.2.5a413.dist-info}/RECORD +5 -5
- {sapiopycommons-2025.2.3a411.dist-info → sapiopycommons-2025.2.5a413.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.2.3a411.dist-info → sapiopycommons-2025.2.5a413.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import base64
|
|
2
|
+
from typing import Final, Mapping, Any
|
|
2
3
|
|
|
3
4
|
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
4
5
|
from sapiopylib.rest.ELNService import ElnManager
|
|
@@ -18,6 +19,11 @@ from sapiopylib.rest.utils.Protocols import ElnEntryStep, ElnExperimentProtocol
|
|
|
18
19
|
|
|
19
20
|
from sapiopycommons.general.exceptions import SapioException
|
|
20
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
|
+
|
|
21
27
|
|
|
22
28
|
# FR-47422: Create utility methods to assist the tool of tools.
|
|
23
29
|
def create_tot_headers(url: str, username: str, password: str, experiment_id: int, tab_prefix: str) \
|
|
@@ -38,23 +44,36 @@ def create_tot_headers(url: str, username: str, password: str, experiment_id: in
|
|
|
38
44
|
# and finally convert the result back into a string.
|
|
39
45
|
encoded_credentials: str = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
|
|
40
46
|
headers: dict[str, str] = {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
45
51
|
}
|
|
46
52
|
return encoded_credentials, headers
|
|
47
53
|
|
|
48
54
|
|
|
49
|
-
def create_user_from_tot_headers(headers:
|
|
55
|
+
def create_user_from_tot_headers(headers: Mapping[str, str]) -> SapioUser:
|
|
50
56
|
"""
|
|
51
57
|
Create a SapioUser object from the headers passed to a tool of tools endpoint.
|
|
52
58
|
|
|
53
59
|
:param headers: The headers that were passed to the endpoint.
|
|
54
60
|
:return: A SapioUser object created from the headers that can be used to communicate with the Sapio server.
|
|
55
61
|
"""
|
|
56
|
-
|
|
57
|
-
|
|
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()}
|
|
58
77
|
|
|
59
78
|
|
|
60
79
|
class ToolOfToolsHelper:
|
|
@@ -82,18 +101,19 @@ class ToolOfToolsHelper:
|
|
|
82
101
|
"""Whether a tab for this tool has been initialized."""
|
|
83
102
|
tab: ElnExperimentTab
|
|
84
103
|
"""The tab that contains the tool's entries."""
|
|
85
|
-
description_entry: ElnEntryStep
|
|
104
|
+
description_entry: ElnEntryStep | None
|
|
86
105
|
"""The text entry that displays the description of the tool."""
|
|
87
|
-
progress_entry: ElnEntryStep
|
|
106
|
+
progress_entry: ElnEntryStep | None
|
|
88
107
|
"""A hidden entry for tracking the progress of the tool."""
|
|
89
|
-
progress_record: DataRecord
|
|
108
|
+
progress_record: DataRecord | None
|
|
90
109
|
"""The record that stores the progress of the tool."""
|
|
91
|
-
progress_gauge_entry: ElnEntryStep
|
|
110
|
+
progress_gauge_entry: ElnEntryStep | None
|
|
92
111
|
"""A chart entry that displays the progress of the tool using the hidden progress entry."""
|
|
93
112
|
results_entry: ElnEntryStep | None
|
|
94
113
|
"""An entry for displaying the results of the tool. If None, the tool does not produce result records."""
|
|
95
114
|
|
|
96
|
-
def __init__(self, headers:
|
|
115
|
+
def __init__(self, headers: Mapping[str, str], name: str, description: str,
|
|
116
|
+
results_data_type: str | None = None):
|
|
97
117
|
"""
|
|
98
118
|
:param headers: The headers that were passed to the endpoint.
|
|
99
119
|
:param name: The name of the tool.
|
|
@@ -101,9 +121,10 @@ class ToolOfToolsHelper:
|
|
|
101
121
|
:param results_data_type: The data type name for the results of the tool. If None, the tool does not produce
|
|
102
122
|
result records.
|
|
103
123
|
"""
|
|
124
|
+
headers: dict[str, str] = format_tot_headers(headers)
|
|
104
125
|
self.user = create_user_from_tot_headers(headers)
|
|
105
|
-
self.exp_id = int(headers[
|
|
106
|
-
self.tab_prefix = headers[
|
|
126
|
+
self.exp_id = int(headers[EXP_ID_HEADER.lower()])
|
|
127
|
+
self.tab_prefix = headers[TAB_PREFIX_HEADER.lower()]
|
|
107
128
|
# The experiment name and record ID aren't necessary to know.
|
|
108
129
|
self._protocol = ElnExperimentProtocol(ElnExperiment(self.exp_id, "", 0), self.user)
|
|
109
130
|
|
|
@@ -121,9 +142,50 @@ class ToolOfToolsHelper:
|
|
|
121
142
|
return self.tab
|
|
122
143
|
self._initialized = True
|
|
123
144
|
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
|
|
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] if entry.get_data_type_names() else None
|
|
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, "results_entry")):
|
|
172
|
+
self.results_entry = entry
|
|
173
|
+
|
|
174
|
+
if not hasattr(self, "progress_entry"):
|
|
175
|
+
self.progress_entry = None
|
|
176
|
+
self.progress_record = None
|
|
177
|
+
if not hasattr(self, "progress_gauge_entry"):
|
|
178
|
+
self.progress_gauge_entry = None
|
|
179
|
+
if not hasattr(self, "description_entry"):
|
|
180
|
+
self.description_entry = None
|
|
181
|
+
if not hasattr(self, "results_entry"):
|
|
182
|
+
self.results_entry = None
|
|
183
|
+
|
|
184
|
+
self.tab = tab
|
|
185
|
+
return tab
|
|
186
|
+
|
|
187
|
+
# Otherwise, create the tab for the tool progress and results.
|
|
188
|
+
tab_crit = ElnExperimentTabAddCriteria(tab_name, [])
|
|
127
189
|
tab: ElnExperimentTab = self.eln_man.add_tab_for_experiment(self.exp_id, tab_crit)
|
|
128
190
|
self.tab = tab
|
|
129
191
|
|
|
@@ -149,7 +211,7 @@ class ToolOfToolsHelper:
|
|
|
149
211
|
|
|
150
212
|
# Create a gauge entry to display the progress.
|
|
151
213
|
gauge_entry: ElnEntryStep = self._create_gauge_chart(self._protocol, progress_entry,
|
|
152
|
-
f"{self.name} Progress", "Progress")
|
|
214
|
+
f"{self.name} Progress", "Progress", "StatusMsg")
|
|
153
215
|
self.progress_gauge_entry = gauge_entry
|
|
154
216
|
|
|
155
217
|
# Create the text entry that displays the description of the tool.
|
|
@@ -158,7 +220,8 @@ class ToolOfToolsHelper:
|
|
|
158
220
|
|
|
159
221
|
# Create a results entry if this tool produces result records.
|
|
160
222
|
if self.results_data_type:
|
|
161
|
-
results_entry = ELNStepFactory.create_table_step(self._protocol, f"{self.name} Results",
|
|
223
|
+
results_entry = ELNStepFactory.create_table_step(self._protocol, f"{self.name} Results",
|
|
224
|
+
self.results_data_type)
|
|
162
225
|
self.results_entry = results_entry
|
|
163
226
|
else:
|
|
164
227
|
self.results_entry = None
|
|
@@ -237,7 +300,8 @@ class ToolOfToolsHelper:
|
|
|
237
300
|
# TODO: Remove this once pylib has a gauge chart function in ElnStepFactory.
|
|
238
301
|
@staticmethod
|
|
239
302
|
def _create_gauge_chart(protocol: ElnExperimentProtocol, data_source_step: ElnEntryStep, step_name: str,
|
|
240
|
-
field_name: str, group_by_field_name: str = "DataRecordName")
|
|
303
|
+
field_name: str, status_field: str, group_by_field_name: str = "DataRecordName") \
|
|
304
|
+
-> ElnEntryStep:
|
|
241
305
|
"""
|
|
242
306
|
Create a gauge chart step in the experiment protocol.
|
|
243
307
|
"""
|
|
@@ -246,7 +310,9 @@ class ToolOfToolsHelper:
|
|
|
246
310
|
data_type_name: str = data_source_step.get_data_type_names()[0]
|
|
247
311
|
series = GaugeChartSeries(data_type_name, field_name)
|
|
248
312
|
series.operation_type = ChartOperationType.VALUE
|
|
249
|
-
chart =
|
|
313
|
+
chart = _GaugeChartDefinition()
|
|
314
|
+
chart.main_data_type_name = data_type_name
|
|
315
|
+
chart.status_field = status_field
|
|
250
316
|
chart.minimum_value = 0.
|
|
251
317
|
chart.maximum_value = 100.
|
|
252
318
|
chart.series_list = [series]
|
|
@@ -259,6 +325,17 @@ class ToolOfToolsHelper:
|
|
|
259
325
|
|
|
260
326
|
|
|
261
327
|
# TODO: This is only here because the get_chart_type function in pylib is wrong. Remove this once pylib is fixed.
|
|
262
|
-
|
|
328
|
+
# Also using this to set the new status field setting.
|
|
329
|
+
class _GaugeChartDefinition(GaugeChartDefinition):
|
|
330
|
+
status_field: str
|
|
331
|
+
|
|
263
332
|
def get_chart_type(self) -> ChartType:
|
|
264
333
|
return ChartType.GAUGE_CHART
|
|
334
|
+
|
|
335
|
+
def to_json(self) -> dict[str, Any]:
|
|
336
|
+
result = super().to_json()
|
|
337
|
+
result["statusValueField"] = {
|
|
338
|
+
"dataTypeName": self.main_data_type_name,
|
|
339
|
+
"dataFieldName": self.status_field
|
|
340
|
+
}
|
|
341
|
+
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sapiopycommons
|
|
3
|
-
Version: 2025.2.
|
|
3
|
+
Version: 2025.2.5a413
|
|
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>
|
|
@@ -15,7 +15,7 @@ sapiopycommons/datatype/attachment_util.py,sha256=_l2swuP8noIGAl4bwzBUEhr6YlN_OV
|
|
|
15
15
|
sapiopycommons/datatype/data_fields.py,sha256=C6HpqtEuF0KsxhlBUprfyv1XguaXql3EYWVbh8y-IFU,4064
|
|
16
16
|
sapiopycommons/datatype/pseudo_data_types.py,sha256=6TG7aJxgmUZ8FQkWBcgmbK5oy7AFFNtKOPpi1w1OOYA,27657
|
|
17
17
|
sapiopycommons/elain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
sapiopycommons/elain/tool_of_tools.py,sha256=
|
|
18
|
+
sapiopycommons/elain/tool_of_tools.py,sha256=pDunj_TReUdyOJPVBIQPnMomSmb6XKp7Dld7ALtP9pE,16998
|
|
19
19
|
sapiopycommons/eln/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
sapiopycommons/eln/experiment_handler.py,sha256=8hmR7sawDo9K6iBwB44QSoxlH1M91inor7dfuXQ4LKs,69323
|
|
21
21
|
sapiopycommons/eln/experiment_report_util.py,sha256=EA2Iq8gW17bSEI6lPoHYQQ-fDvG4O28RWOoTPXpOlUw,36640
|
|
@@ -57,7 +57,7 @@ sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
|
57
57
|
sapiopycommons/webhook/webhook_context.py,sha256=D793uLsb1691SalaPnBUk3rOSxn_hYLhdvkaIxjNXss,1909
|
|
58
58
|
sapiopycommons/webhook/webhook_handlers.py,sha256=M5PMt-j7PpnzUQMUQDTvqwJUyJNxuFtC9wdnk5VRNpI,39703
|
|
59
59
|
sapiopycommons/webhook/webservice_handlers.py,sha256=Y5dHx_UFWFuSqaoPL6Re-fsKYRuxvCWZ8bj6KSZ3jfM,14285
|
|
60
|
-
sapiopycommons-2025.2.
|
|
61
|
-
sapiopycommons-2025.2.
|
|
62
|
-
sapiopycommons-2025.2.
|
|
63
|
-
sapiopycommons-2025.2.
|
|
60
|
+
sapiopycommons-2025.2.5a413.dist-info/METADATA,sha256=GgQkRbuFo__5KS44hO-eC1TNlNvSODz1HCmt9HLX7H8,3142
|
|
61
|
+
sapiopycommons-2025.2.5a413.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
62
|
+
sapiopycommons-2025.2.5a413.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
|
|
63
|
+
sapiopycommons-2025.2.5a413.dist-info/RECORD,,
|
|
File without changes
|
{sapiopycommons-2025.2.3a411.dist-info → sapiopycommons-2025.2.5a413.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|