sapiopycommons 2025.2.14a436__py3-none-any.whl → 2025.2.17a438__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/ai/tool_of_tools.py +72 -8
- {sapiopycommons-2025.2.14a436.dist-info → sapiopycommons-2025.2.17a438.dist-info}/METADATA +1 -1
- {sapiopycommons-2025.2.14a436.dist-info → sapiopycommons-2025.2.17a438.dist-info}/RECORD +5 -5
- {sapiopycommons-2025.2.14a436.dist-info → sapiopycommons-2025.2.17a438.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.2.14a436.dist-info → sapiopycommons-2025.2.17a438.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,12 +6,16 @@ from typing import Final, Mapping, Any
|
|
|
6
6
|
|
|
7
7
|
from pandas import DataFrame
|
|
8
8
|
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
9
|
+
from sapiopylib.rest.DataTypeService import DataTypeManager
|
|
9
10
|
from sapiopylib.rest.ELNService import ElnManager
|
|
10
11
|
from sapiopylib.rest.User import SapioUser
|
|
11
12
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
13
|
+
from sapiopylib.rest.pojo.Sort import SortDirection
|
|
12
14
|
from sapiopylib.rest.pojo.chartdata.DashboardDefinition import GaugeChartDefinition
|
|
13
15
|
from sapiopylib.rest.pojo.chartdata.DashboardEnums import ChartGroupingType, ChartOperationType
|
|
14
16
|
from sapiopylib.rest.pojo.chartdata.DashboardSeries import GaugeChartSeries
|
|
17
|
+
from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
|
|
18
|
+
from sapiopylib.rest.pojo.datatype.DataTypeLayout import DataTypeLayout, TableLayout
|
|
15
19
|
from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition, FieldType
|
|
16
20
|
from sapiopylib.rest.pojo.eln.ElnEntryPosition import ElnEntryPosition
|
|
17
21
|
from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
|
|
@@ -55,6 +59,9 @@ def create_tot_headers(url: str, username: str, password: str, experiment_id: in
|
|
|
55
59
|
# Encode the credentials to bytes, then encode them using base64,
|
|
56
60
|
# and finally convert the result back into a string.
|
|
57
61
|
encoded_credentials: str = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
|
|
62
|
+
# Remove the trailing slash from the URL if it exists.
|
|
63
|
+
if url.endswith("/"):
|
|
64
|
+
url.rstrip("/")
|
|
58
65
|
headers: dict[str, str] = {
|
|
59
66
|
CREDENTIALS_HEADER: f"Basic {encoded_credentials}",
|
|
60
67
|
API_URL_HEADER: url,
|
|
@@ -74,7 +81,10 @@ def create_user_from_tot_headers(headers: Mapping[str, str]) -> SapioUser:
|
|
|
74
81
|
headers: dict[str, str] = format_tot_headers(headers)
|
|
75
82
|
credentials = (base64.b64decode(headers[CREDENTIALS_HEADER.lower()].removeprefix("Basic "))
|
|
76
83
|
.decode("utf-8").split(":", 1))
|
|
77
|
-
|
|
84
|
+
url: str = headers[API_URL_HEADER.lower()]
|
|
85
|
+
if url.endswith("/"):
|
|
86
|
+
url.rstrip("/")
|
|
87
|
+
return SapioUser(url, username=credentials[0], password=credentials[1])
|
|
78
88
|
|
|
79
89
|
|
|
80
90
|
def format_tot_headers(headers: Mapping[str, str]) -> dict[str, str]:
|
|
@@ -181,6 +191,7 @@ class AiHelper:
|
|
|
181
191
|
# Managers.
|
|
182
192
|
dr_man: DataRecordManager
|
|
183
193
|
eln_man: ElnManager
|
|
194
|
+
dt_man: DataTypeManager
|
|
184
195
|
|
|
185
196
|
def __init__(self, user: SapioUser, exp_id: int):
|
|
186
197
|
"""
|
|
@@ -192,6 +203,7 @@ class AiHelper:
|
|
|
192
203
|
|
|
193
204
|
self.dr_man = DataRecordManager(self.user)
|
|
194
205
|
self.eln_man = ElnManager(self.user)
|
|
206
|
+
self.dt_man = DataTypeManager(self.user)
|
|
195
207
|
|
|
196
208
|
@property
|
|
197
209
|
def protocol(self) -> ElnExperimentProtocol:
|
|
@@ -226,24 +238,42 @@ class AiHelper:
|
|
|
226
238
|
def create_experiment_details_from_data_frame(self,
|
|
227
239
|
tab: ElnExperimentTab,
|
|
228
240
|
entry_name: str,
|
|
229
|
-
df: DataFrame
|
|
241
|
+
df: DataFrame,
|
|
242
|
+
sort_field: str | None = None,
|
|
243
|
+
sort_direction: SortDirection = SortDirection.DESCENDING,
|
|
244
|
+
smiles_column: str | None = None) -> ExperimentEntry | None:
|
|
230
245
|
"""
|
|
231
246
|
Create an experiment detail entry from a DataFrame.
|
|
232
247
|
|
|
233
248
|
:param tab: The tab that the entry should be added to.
|
|
234
249
|
:param entry_name: The name of the entry.
|
|
235
250
|
:param df: The DataFrame to create the entry from.
|
|
251
|
+
:param sort_field: The field to sort the resulting entry rows by, if any.
|
|
252
|
+
:param sort_direction: The direction to sort the resulting entry rows in, if a sort_field is provided.
|
|
253
|
+
:param smiles_column: The column name in the provided DataFrame that corresponds to the SMILES strings of the
|
|
254
|
+
compounds tracked in the DataFrame, if any. If this is provided, then the entry will be created with
|
|
255
|
+
images of the compounds corresponding to the SMILES strings in each row of the table.
|
|
236
256
|
:return: The newly created experiment detail entry.
|
|
237
257
|
"""
|
|
238
258
|
json_list: list[dict[str, Any]] = []
|
|
259
|
+
smiles: list[str] = []
|
|
239
260
|
for _, row in df.iterrows():
|
|
240
|
-
|
|
241
|
-
|
|
261
|
+
row_dict: dict[str, Any] = row.to_dict()
|
|
262
|
+
if smiles_column is not None:
|
|
263
|
+
smiles.append(row_dict.get(smiles_column))
|
|
264
|
+
json_list.append(row_dict)
|
|
265
|
+
images: list[bytes] | None = None
|
|
266
|
+
if smiles:
|
|
267
|
+
images = self.smiles_to_svg(smiles)
|
|
268
|
+
return self.create_experiment_details_from_json(tab, entry_name, json_list, sort_field, sort_direction, images)
|
|
242
269
|
|
|
243
270
|
def create_experiment_details_from_json(self,
|
|
244
271
|
tab: ElnExperimentTab,
|
|
245
272
|
entry_name: str,
|
|
246
|
-
json_list: list[dict[str, Any]]
|
|
273
|
+
json_list: list[dict[str, Any]],
|
|
274
|
+
sort_field: str | None = None,
|
|
275
|
+
sort_direction: SortDirection = SortDirection.DESCENDING,
|
|
276
|
+
images: list[bytes] | None = None) -> ExperimentEntry | None:
|
|
247
277
|
"""
|
|
248
278
|
Create an experiment detail entry from a list of JSON dictionaries.
|
|
249
279
|
|
|
@@ -251,6 +281,11 @@ class AiHelper:
|
|
|
251
281
|
:param entry_name: The name of the entry.
|
|
252
282
|
:param json_list: The list of JSON dictionaries to create the entry from. Each dictionary is expected to have the
|
|
253
283
|
same keys.
|
|
284
|
+
:param sort_field: The field to sort the resulting entry rows by, if any.
|
|
285
|
+
:param sort_direction: The direction to sort the resulting entry rows in, if a sort_field is provided.
|
|
286
|
+
:param images: The images to include in the entry, if any. The images will be added to the rows that they
|
|
287
|
+
correspond to based on the order of the images in the images list and the order of the rows in the
|
|
288
|
+
json list.
|
|
254
289
|
:return: The newly created experiment detail entry.
|
|
255
290
|
"""
|
|
256
291
|
if not json_list:
|
|
@@ -271,12 +306,26 @@ class AiHelper:
|
|
|
271
306
|
fields.append(field)
|
|
272
307
|
fields_by_name[key] = field
|
|
273
308
|
|
|
309
|
+
# Sort the JSON list if requested.
|
|
310
|
+
if sort_field and sort_direction != SortDirection.NONE:
|
|
311
|
+
if images:
|
|
312
|
+
old_order: list[str] = [x[sort_field] for x in json_list]
|
|
313
|
+
json_list.sort(key=lambda x: x.get(sort_field), reverse=sort_direction == SortDirection.DESCENDING)
|
|
314
|
+
# We'll need to resort the images as well.
|
|
315
|
+
if images:
|
|
316
|
+
new_order: list[str] = [x[sort_field] for x in json_list]
|
|
317
|
+
new_images: list[bytes] = []
|
|
318
|
+
for val in new_order:
|
|
319
|
+
# noinspection PyUnboundLocalVariable
|
|
320
|
+
new_images.append(images[old_order.index(val)])
|
|
321
|
+
images = new_images
|
|
322
|
+
|
|
274
323
|
# Extract the valid field values from the JSON.
|
|
275
324
|
field_maps: list[dict[str, Any]] = []
|
|
276
325
|
for json_dict in json_list:
|
|
277
326
|
field_map: dict[str, Any] = {}
|
|
278
327
|
for key, field in fields_by_name.items():
|
|
279
|
-
# Watch out for NaN values or other special values.
|
|
328
|
+
# Watch out for NaN values or other special values in numeric columns.
|
|
280
329
|
val: Any = json_dict.get(key)
|
|
281
330
|
if (field.data_field_type == FieldType.DOUBLE
|
|
282
331
|
and (not isinstance(val, (int, float))) or (isinstance(val, float) and math.isnan(val))):
|
|
@@ -284,13 +333,28 @@ class AiHelper:
|
|
|
284
333
|
field_map[field.data_field_name] = val
|
|
285
334
|
field_maps.append(field_map)
|
|
286
335
|
|
|
336
|
+
# Create the experiment detail entry.
|
|
287
337
|
detail_entry = ElnEntryCriteria(ElnEntryType.Table, entry_name,
|
|
288
338
|
ElnBaseDataType.EXPERIMENT_DETAIL.data_type_name,
|
|
289
339
|
self.tab_next_entry_order(tab),
|
|
290
340
|
notebook_experiment_tab_id=tab.tab_id,
|
|
291
341
|
field_definition_list=fields)
|
|
292
342
|
entry = self.eln_man.add_experiment_entry(self.exp_id, detail_entry)
|
|
293
|
-
self.dr_man.add_data_records_with_data(entry.data_type_name, field_maps)
|
|
343
|
+
records: list[DataRecord] = self.dr_man.add_data_records_with_data(entry.data_type_name, field_maps)
|
|
344
|
+
|
|
345
|
+
# If images are provided, update the data type definition of the experiment detail data type to allow
|
|
346
|
+
# record images and add the images to the records.
|
|
347
|
+
if images:
|
|
348
|
+
dt: DataTypeDefinition = self.dt_man.get_data_type_definition(entry.data_type_name)
|
|
349
|
+
dt.is_record_image_assignable = True
|
|
350
|
+
self.eln_man.update_eln_data_type_definition(self.exp_id, entry.entry_id, dt)
|
|
351
|
+
|
|
352
|
+
layout: DataTypeLayout = self.dt_man.get_default_layout(entry.data_type_name)
|
|
353
|
+
layout.table_layout = TableLayout(cell_size=128, record_image_width=128)
|
|
354
|
+
self.eln_man.update_eln_data_type_layout(self.exp_id, entry.entry_id, layout)
|
|
355
|
+
|
|
356
|
+
self.update_record_images(records, images)
|
|
357
|
+
|
|
294
358
|
return entry
|
|
295
359
|
|
|
296
360
|
def create_text_entry(self, tab: ElnExperimentTab, timestamp: str, description: str, auto_format: bool = True) \
|
|
@@ -538,7 +602,7 @@ class ToolOfToolsHelper:
|
|
|
538
602
|
x.field_set_name == "Tool of Tools Progress"]
|
|
539
603
|
if not progress_field_set:
|
|
540
604
|
raise SapioException("Unable to locate the field set for the Tool of Tools progress.")
|
|
541
|
-
progress_entry_crit = ElnEntryCriteria(ElnEntryType.Form, f"
|
|
605
|
+
progress_entry_crit = ElnEntryCriteria(ElnEntryType.Form, f"{tab_name} Progress",
|
|
542
606
|
ElnBaseDataType.EXPERIMENT_DETAIL.data_type_name, 1,
|
|
543
607
|
notebook_experiment_tab_id=self.tab.tab_id,
|
|
544
608
|
enb_field_set_id=progress_field_set[0].field_set_id)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sapiopycommons
|
|
3
|
-
Version: 2025.2.
|
|
3
|
+
Version: 2025.2.17a438
|
|
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>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
sapiopycommons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
sapiopycommons/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
sapiopycommons/ai/tool_of_tools.py,sha256=
|
|
3
|
+
sapiopycommons/ai/tool_of_tools.py,sha256=IESa3NCMTUa5ILLhZzJeRH1BcltesDcvOBpFHQtlwII,37622
|
|
4
4
|
sapiopycommons/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
sapiopycommons/callbacks/callback_util.py,sha256=68lPK8GtE6zd8C0fQaMLxT57PtU-6HnO37Zk-u56Mrs,129933
|
|
6
6
|
sapiopycommons/callbacks/field_builder.py,sha256=p2XacN99MuKk3ite8GAqstUMpixqugul2CsC4gB83-o,38620
|
|
@@ -58,7 +58,7 @@ sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
|
58
58
|
sapiopycommons/webhook/webhook_context.py,sha256=D793uLsb1691SalaPnBUk3rOSxn_hYLhdvkaIxjNXss,1909
|
|
59
59
|
sapiopycommons/webhook/webhook_handlers.py,sha256=L0HetSm43NvA5KyW3xbLpGFh2DbAaeZJVtXIEl2fvV8,39689
|
|
60
60
|
sapiopycommons/webhook/webservice_handlers.py,sha256=Y5dHx_UFWFuSqaoPL6Re-fsKYRuxvCWZ8bj6KSZ3jfM,14285
|
|
61
|
-
sapiopycommons-2025.2.
|
|
62
|
-
sapiopycommons-2025.2.
|
|
63
|
-
sapiopycommons-2025.2.
|
|
64
|
-
sapiopycommons-2025.2.
|
|
61
|
+
sapiopycommons-2025.2.17a438.dist-info/METADATA,sha256=b4DM-Ldf6nJpnk4JcQ9m1QmGVB2sfcCf_xcGFLJ5TVw,3143
|
|
62
|
+
sapiopycommons-2025.2.17a438.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
63
|
+
sapiopycommons-2025.2.17a438.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
|
|
64
|
+
sapiopycommons-2025.2.17a438.dist-info/RECORD,,
|
|
File without changes
|
{sapiopycommons-2025.2.14a436.dist-info → sapiopycommons-2025.2.17a438.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|