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.

@@ -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
- "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
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: dict[str, str]) -> SapioUser:
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
- 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])
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: dict[str, str], name: str, description: str, results_data_type: str | None = None):
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["EXPERIMENT_ID"])
106
- self.tab_prefix = headers["TAB_PREFIX"]
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
- # 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}", [])
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", self.results_data_type)
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") -> ElnEntryStep:
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 = _FixedGaugeChartDefinition()
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
- class _FixedGaugeChartDefinition(GaugeChartDefinition):
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.3a411
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=ZwLx2wA2wRhQAXaCpT-KRWXnD-W8eqoULX5DKOH99ak,13486
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.3a411.dist-info/METADATA,sha256=9i974-D51GUNUE4IM2vI6Z0Ok_zo1zs05WatbiNMjrE,3142
61
- sapiopycommons-2025.2.3a411.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
62
- sapiopycommons-2025.2.3a411.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
63
- sapiopycommons-2025.2.3a411.dist-info/RECORD,,
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,,