sapiopycommons 2025.7.31a664__tar.gz → 2025.8.1a670__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.
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/PKG-INFO +2 -2
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/pyproject.toml +2 -2
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/ai/tool_of_tools.py +235 -127
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/callbacks/callback_util.py +21 -14
- sapiopycommons-2025.8.1a670/src/sapiopycommons/files/assay_plate_reader.py +93 -0
- sapiopycommons-2025.8.1a670/src/sapiopycommons/files/file_text_converter.py +207 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/flowcyto/flow_cyto.py +2 -24
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/general/accession_service.py +2 -28
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/multimodal/multimodal.py +2 -24
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/accession_test.py +1 -1
- sapiopycommons-2025.8.1a670/tests/assay_plate_reader/BMGLabtech96.txt +28 -0
- sapiopycommons-2025.8.1a670/tests/assay_plate_reader/assay_plate_processing_test.py +43 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/mafft_test.py +1 -1
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/.gitignore +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/LICENSE +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/README.md +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/ai/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/callbacks/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/callbacks/field_builder.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/chem/IndigoMolecules.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/chem/Molecules.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/chem/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/customreport/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/customreport/auto_pagers.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/customreport/column_builder.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/customreport/custom_report_builder.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/customreport/term_builder.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/datatype/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/datatype/attachment_util.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/datatype/data_fields.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/datatype/pseudo_data_types.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/eln/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/eln/experiment_cache.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/eln/experiment_handler.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/eln/experiment_report_util.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/eln/experiment_step_factory.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/eln/experiment_tags.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/eln/plate_designer.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/eln/step_creation.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/files/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/files/complex_data_loader.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/files/file_bridge.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/files/file_bridge_handler.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/files/file_data_handler.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/files/file_util.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/files/file_validator.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/files/file_writer.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/flowcyto/flowcyto_data.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/general/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/general/aliases.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/general/audit_log.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/general/custom_report_util.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/general/data_structure_util.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/general/directive_util.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/general/exceptions.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/general/html_formatter.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/general/popup_util.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/general/sapio_links.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/general/storage_util.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/general/time_util.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/multimodal/multimodal_data.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/processtracking/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/processtracking/custom_workflow_handler.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/processtracking/endpoints.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/recordmodel/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/recordmodel/record_handler.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/rules/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/rules/eln_rule_handler.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/rules/on_save_rule_handler.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/samples/aliquot.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/sftpconnect/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/sftpconnect/sftp_builder.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/webhook/__init__.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/webhook/webhook_context.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/webhook/webhook_handlers.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/webhook/webservice_handlers.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/AF-A0A009IHW8-F1-model_v4.cif +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/_do_not_add_init_py_here +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/aliquot_test.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/bio_reg_test.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/chem_test.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/chem_test_curation_queue.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/curation_queue_test.sdf +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/data_type_models.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/flowcyto/101_DEN084Y5_15_E01_008_clean.fcs +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/flowcyto/101_DEN084Y5_15_E03_009_clean.fcs +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/flowcyto/101_DEN084Y5_15_E05_010_clean.fcs +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/flowcyto/8_color_ICS.wsp +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/flowcyto/COVID19_W_001_O.fcs +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/flowcyto_test.py +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/kappa.chains.fasta +0 -0
- {sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/tests/test.gb +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sapiopycommons
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.8.1a670
|
|
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>
|
|
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
18
|
Requires-Python: >=3.10
|
|
19
19
|
Requires-Dist: databind>=4.5
|
|
20
|
-
Requires-Dist: sapiopylib>=2025.
|
|
20
|
+
Requires-Dist: sapiopylib>=2025.7.31a279
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
|
|
23
23
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sapiopycommons"
|
|
7
|
-
version='2025.
|
|
7
|
+
version='2025.08.01a670'
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Jonathan Steck", email="jsteck@sapiosciences.com" },
|
|
10
10
|
{ name="Yechen Qiao", email="yqiao@sapiosciences.com" },
|
|
@@ -14,7 +14,7 @@ license = "MPL-2.0"
|
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
requires-python = ">=3.10"
|
|
16
16
|
dependencies = [
|
|
17
|
-
'sapiopylib>=2025.
|
|
17
|
+
'sapiopylib>=2025.7.31a279', 'databind>=4.5'
|
|
18
18
|
]
|
|
19
19
|
classifiers = [
|
|
20
20
|
"Intended Audience :: Developers",
|
{sapiopycommons-2025.7.31a664 → sapiopycommons-2025.8.1a670}/src/sapiopycommons/ai/tool_of_tools.py
RENAMED
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import io
|
|
3
3
|
import math
|
|
4
|
-
import re
|
|
5
4
|
from typing import Final, Mapping, Any
|
|
6
5
|
|
|
7
6
|
import requests
|
|
8
7
|
from pandas import DataFrame
|
|
9
8
|
from requests import Response
|
|
9
|
+
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
10
10
|
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
11
11
|
from sapiopylib.rest.DataTypeService import DataTypeManager
|
|
12
12
|
from sapiopylib.rest.ELNService import ElnManager
|
|
13
13
|
from sapiopylib.rest.User import SapioUser
|
|
14
14
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
15
15
|
from sapiopylib.rest.pojo.Sort import SortDirection
|
|
16
|
-
from sapiopylib.rest.pojo.chartdata.DashboardDefinition import GaugeChartDefinition
|
|
17
|
-
from sapiopylib.rest.pojo.chartdata.DashboardEnums import ChartGroupingType, ChartOperationType
|
|
16
|
+
from sapiopylib.rest.pojo.chartdata.DashboardDefinition import GaugeChartDefinition, DashboardDefinition
|
|
17
|
+
from sapiopylib.rest.pojo.chartdata.DashboardEnums import ChartGroupingType, ChartOperationType, DashboardScope
|
|
18
18
|
from sapiopylib.rest.pojo.chartdata.DashboardSeries import GaugeChartSeries
|
|
19
19
|
from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
|
|
20
20
|
from sapiopylib.rest.pojo.datatype.DataTypeLayout import DataTypeLayout, TableLayout
|
|
21
|
-
from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition, FieldType
|
|
21
|
+
from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition, FieldType, \
|
|
22
|
+
VeloxStringFieldDefinition
|
|
22
23
|
from sapiopylib.rest.pojo.eln.ElnEntryPosition import ElnEntryPosition
|
|
23
24
|
from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
|
|
24
25
|
from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry
|
|
25
26
|
from sapiopylib.rest.pojo.eln.ExperimentEntryCriteria import ElnEntryCriteria, ElnFormEntryUpdateCriteria, \
|
|
26
|
-
ElnDashboardEntryUpdateCriteria
|
|
27
|
-
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnEntryType, ElnBaseDataType
|
|
27
|
+
ElnDashboardEntryUpdateCriteria
|
|
28
|
+
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnEntryType, ElnBaseDataType, ExperimentEntryStatus
|
|
28
29
|
from sapiopylib.rest.pojo.eln.eln_headings import ElnExperimentTabAddCriteria, ElnExperimentTab
|
|
29
30
|
from sapiopylib.rest.pojo.eln.field_set import ElnFieldSetInfo
|
|
30
31
|
from sapiopylib.rest.utils.ProtocolUtils import ELNStepFactory
|
|
@@ -33,6 +34,7 @@ from sapiopylib.rest.utils.Protocols import ElnEntryStep, ElnExperimentProtocol
|
|
|
33
34
|
from sapiopycommons.callbacks.field_builder import FieldBuilder
|
|
34
35
|
from sapiopycommons.general.aliases import AliasUtil, SapioRecord
|
|
35
36
|
from sapiopycommons.general.exceptions import SapioException
|
|
37
|
+
from sapiopycommons.general.html_formatter import HtmlFormatter
|
|
36
38
|
from sapiopycommons.general.time_util import TimeUtil
|
|
37
39
|
from sapiopycommons.multimodal.multimodal import MultiModalManager
|
|
38
40
|
from sapiopycommons.multimodal.multimodal_data import ImageDataRequestPojo
|
|
@@ -100,88 +102,6 @@ def format_tot_headers(headers: Mapping[str, str]) -> dict[str, str]:
|
|
|
100
102
|
return {k.lower(): v for k, v in headers.items()}
|
|
101
103
|
|
|
102
104
|
|
|
103
|
-
class HtmlFormatter:
|
|
104
|
-
"""
|
|
105
|
-
A class for formatting text in HTML with tag classes supported by the client.
|
|
106
|
-
"""
|
|
107
|
-
TIMESTAMP_TEXT__CSS_CLASS_NAME: Final[str] = "timestamp-text"
|
|
108
|
-
HEADER_1_TEXT__CSS_CLASS_NAME: Final[str] = "header1-text"
|
|
109
|
-
HEADER_2_TEXT__CSS_CLASS_NAME: Final[str] = "header2-text"
|
|
110
|
-
HEADER_3_TEXT__CSS_CLASS_NAME: Final[str] = "header3-text"
|
|
111
|
-
BODY_TEXT__CSS_CLASS_NAME: Final[str] = "body-text"
|
|
112
|
-
CAPTION_TEXT__CSS_CLASS_NAME: Final[str] = "caption-text"
|
|
113
|
-
|
|
114
|
-
@staticmethod
|
|
115
|
-
def timestamp(text: str) -> str:
|
|
116
|
-
"""
|
|
117
|
-
Given a text string, return that same text string HTML formatted using the timestamp CSS class.
|
|
118
|
-
|
|
119
|
-
:param text: The text to format.
|
|
120
|
-
:return: The HTML formatted text.
|
|
121
|
-
"""
|
|
122
|
-
return f"<span class=\"{HtmlFormatter.TIMESTAMP_TEXT__CSS_CLASS_NAME}\">{text}</span>"
|
|
123
|
-
|
|
124
|
-
@staticmethod
|
|
125
|
-
def header_1(text: str) -> str:
|
|
126
|
-
"""
|
|
127
|
-
Given a text string, return that same text string HTML formatted using the header 1 CSS class.
|
|
128
|
-
|
|
129
|
-
:param text: The text to format.
|
|
130
|
-
:return: The HTML formatted text.
|
|
131
|
-
"""
|
|
132
|
-
return f"<span class=\"{HtmlFormatter.HEADER_1_TEXT__CSS_CLASS_NAME}\">{text}</span>"
|
|
133
|
-
|
|
134
|
-
@staticmethod
|
|
135
|
-
def header_2(text: str) -> str:
|
|
136
|
-
"""
|
|
137
|
-
Given a text string, return that same text string HTML formatted using the header 2 CSS class.
|
|
138
|
-
|
|
139
|
-
:param text: The text to format.
|
|
140
|
-
:return: The HTML formatted text.
|
|
141
|
-
"""
|
|
142
|
-
return f"<span class=\"{HtmlFormatter.HEADER_2_TEXT__CSS_CLASS_NAME}\">{text}</span>"
|
|
143
|
-
|
|
144
|
-
@staticmethod
|
|
145
|
-
def header_3(text: str) -> str:
|
|
146
|
-
"""
|
|
147
|
-
Given a text string, return that same text string HTML formatted using the header 3 CSS class.
|
|
148
|
-
|
|
149
|
-
:param text: The text to format.
|
|
150
|
-
:return: The HTML formatted text.
|
|
151
|
-
"""
|
|
152
|
-
return f"<span class=\"{HtmlFormatter.HEADER_3_TEXT__CSS_CLASS_NAME}\">{text}</span>"
|
|
153
|
-
|
|
154
|
-
@staticmethod
|
|
155
|
-
def body(text: str) -> str:
|
|
156
|
-
"""
|
|
157
|
-
Given a text string, return that same text string HTML formatted using the body CSS class.
|
|
158
|
-
|
|
159
|
-
:param text: The text to format.
|
|
160
|
-
:return: The HTML formatted text.
|
|
161
|
-
"""
|
|
162
|
-
return f"<span class=\"{HtmlFormatter.BODY_TEXT__CSS_CLASS_NAME}\">{text}</span>"
|
|
163
|
-
|
|
164
|
-
@staticmethod
|
|
165
|
-
def caption(text: str) -> str:
|
|
166
|
-
"""
|
|
167
|
-
Given a text string, return that same text string HTML formatted using the caption CSS class.
|
|
168
|
-
|
|
169
|
-
:param text: The text to format.
|
|
170
|
-
:return: The HTML formatted text.
|
|
171
|
-
"""
|
|
172
|
-
return f"<span class=\"{HtmlFormatter.CAPTION_TEXT__CSS_CLASS_NAME}\">{text}</span>"
|
|
173
|
-
|
|
174
|
-
@staticmethod
|
|
175
|
-
def replace_newlines(text: str) -> str:
|
|
176
|
-
"""
|
|
177
|
-
Given a text string, return that same text string HTML formatted with newlines replaced by HTML line breaks.
|
|
178
|
-
|
|
179
|
-
:param text: The text to format.
|
|
180
|
-
:return: The HTML formatted text.
|
|
181
|
-
"""
|
|
182
|
-
return re.sub("\r?\n", "<br>", text)
|
|
183
|
-
|
|
184
|
-
|
|
185
105
|
class AiHelper:
|
|
186
106
|
"""
|
|
187
107
|
A class with helper methods for the AI to make use of when creating/updating experiment tabs and entries.
|
|
@@ -189,25 +109,28 @@ class AiHelper:
|
|
|
189
109
|
# Contextual info.
|
|
190
110
|
user: SapioUser
|
|
191
111
|
exp_id: int
|
|
112
|
+
timeout: int
|
|
192
113
|
|
|
193
114
|
# Managers.
|
|
194
115
|
dr_man: DataRecordManager
|
|
195
116
|
eln_man: ElnManager
|
|
196
117
|
dt_man: DataTypeManager
|
|
197
118
|
|
|
198
|
-
def __init__(self, user: SapioUser, exp_id: int):
|
|
119
|
+
def __init__(self, user: SapioUser, exp_id: int, timeout: int = 120):
|
|
199
120
|
"""
|
|
200
121
|
:param user: The user to send the requests from.
|
|
201
122
|
:param exp_id: The ID of the experiment to create the entries in.
|
|
123
|
+
:param timeout: The timeout in seconds to use for requests.
|
|
202
124
|
"""
|
|
203
125
|
self.user = user
|
|
204
126
|
self.exp_id = exp_id
|
|
127
|
+
self.timeout = timeout
|
|
205
128
|
|
|
206
129
|
self.dr_man = DataRecordManager(self.user)
|
|
207
130
|
self.eln_man = ElnManager(self.user)
|
|
208
131
|
self.dt_man = DataTypeManager(self.user)
|
|
209
132
|
|
|
210
|
-
def
|
|
133
|
+
def call_post_endpoint(self, url: str, payload: Any, tab_prefix: str = "") -> Response:
|
|
211
134
|
"""
|
|
212
135
|
Call a tool endpoint. Constructs the tool headers and checks the response for errors for the caller.
|
|
213
136
|
|
|
@@ -217,7 +140,21 @@ class AiHelper:
|
|
|
217
140
|
:return: The Response object returned by the endpoint.
|
|
218
141
|
"""
|
|
219
142
|
headers = create_tot_headers(self.user.url, self.user.username, self.user.password, self.exp_id, tab_prefix)
|
|
220
|
-
response = requests.post(url, json=payload, headers=headers)
|
|
143
|
+
response = requests.post(url, json=payload, headers=headers, timeout=self.timeout)
|
|
144
|
+
response.raise_for_status()
|
|
145
|
+
return response
|
|
146
|
+
|
|
147
|
+
def call_get_endpoint(self, url: str, params: Any, tab_prefix: str = "") -> Response:
|
|
148
|
+
"""
|
|
149
|
+
Call a tool endpoint. Constructs the tool headers and checks the response for errors for the caller.
|
|
150
|
+
|
|
151
|
+
:param url: The URL of the endpoint to call.
|
|
152
|
+
:param params: The query parameters to send to the endpoint.
|
|
153
|
+
:param tab_prefix: The prefix to use for the tab name that will be created by the tool.
|
|
154
|
+
:return: The Response object returned by the endpoint.
|
|
155
|
+
"""
|
|
156
|
+
headers = create_tot_headers(self.user.url, self.user.username, self.user.password, self.exp_id, tab_prefix)
|
|
157
|
+
response = requests.get(url, params=params, headers=headers, timeout=self.timeout)
|
|
221
158
|
response.raise_for_status()
|
|
222
159
|
return response
|
|
223
160
|
|
|
@@ -307,20 +244,75 @@ class AiHelper:
|
|
|
307
244
|
if not json_list:
|
|
308
245
|
return None
|
|
309
246
|
|
|
247
|
+
def update_string_field(f: AbstractVeloxFieldDefinition, v: Any) -> None:
|
|
248
|
+
"""
|
|
249
|
+
Update the max length of the string field and whether it is a link-out field depending on the length and
|
|
250
|
+
form of the given value.
|
|
251
|
+
|
|
252
|
+
:param f: The definition of the string field.
|
|
253
|
+
:param v: A field value that will be present for this field.
|
|
254
|
+
"""
|
|
255
|
+
if not isinstance(f, VeloxStringFieldDefinition) or v is None:
|
|
256
|
+
return
|
|
257
|
+
sv = str(v)
|
|
258
|
+
f.max_length = max(f.max_length, len(sv))
|
|
259
|
+
if not f.link_out and sv.startswith("http://") or sv.startswith("https://"):
|
|
260
|
+
link_out, link_out_url = FieldBuilder._convert_link_out({"Link": "[[LINK_OUT]]"})
|
|
261
|
+
f.link_out = link_out
|
|
262
|
+
f.link_out_url = link_out_url
|
|
263
|
+
|
|
310
264
|
# Determine which fields in the JSON can be used to create field definitions.
|
|
311
265
|
fb = FieldBuilder()
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
for
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
field
|
|
322
|
-
|
|
323
|
-
|
|
266
|
+
json_key_to_field_def: dict[str, AbstractVeloxFieldDefinition] = {}
|
|
267
|
+
numeric_string_fields: set[str] = set()
|
|
268
|
+
for values in json_list:
|
|
269
|
+
for key, value in values.items():
|
|
270
|
+
# Skip null values, since we can't know what type they're meant to represent.
|
|
271
|
+
if value is None:
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
# The field name is the JSON key name, but with spaces and dashes replaced by underscores and with a
|
|
275
|
+
# leading underscore added if the field name starts with a number.
|
|
276
|
+
field_name: str = key.strip()
|
|
277
|
+
if " " in field_name:
|
|
278
|
+
field_name = field_name.replace(" ", "_")
|
|
279
|
+
if "-" in field_name:
|
|
280
|
+
field_name = field_name.replace("-", "_")
|
|
281
|
+
if field_name[0].isnumeric():
|
|
282
|
+
field_name = "_" + field_name
|
|
283
|
+
|
|
284
|
+
# If this is the first time this key is being encountered, create a field for it.
|
|
285
|
+
if key not in json_key_to_field_def:
|
|
286
|
+
if isinstance(value, str):
|
|
287
|
+
json_key_to_field_def[key] = fb.string_field(field_name, display_name=key)
|
|
288
|
+
update_string_field(json_key_to_field_def[key], value)
|
|
289
|
+
elif isinstance(value, bool):
|
|
290
|
+
json_key_to_field_def[key] = fb.boolean_field(field_name, display_name=key)
|
|
291
|
+
elif isinstance(value, (int, float)):
|
|
292
|
+
json_key_to_field_def[key] = fb.double_field(field_name, display_name=key, precision=3)
|
|
293
|
+
# All other values in the JSON get skipped.
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
# The field definition already exists, but it may not be a valid field type for this value.
|
|
297
|
+
field_type: FieldType = json_key_to_field_def[key].data_field_type
|
|
298
|
+
# Strings can be anything, so we don't need to check the value type.
|
|
299
|
+
if field_type == FieldType.STRING:
|
|
300
|
+
# We still need to make sure the lengths are fine.
|
|
301
|
+
update_string_field(json_key_to_field_def[key], value)
|
|
302
|
+
continue
|
|
303
|
+
# Boolean values can only be booleans.
|
|
304
|
+
if field_type == FieldType.BOOLEAN and isinstance(value, bool):
|
|
305
|
+
continue
|
|
306
|
+
# Integers and floats both fit in DOUBLE fields, but floats can't be NaN or infinity.
|
|
307
|
+
if field_type == FieldType.DOUBLE:
|
|
308
|
+
# Booleans count as ints for isinstance, so make sure that true integers continue but bools don't.
|
|
309
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
310
|
+
continue
|
|
311
|
+
if isinstance(value, float) and not math.isnan(value) and not math.isinf(value):
|
|
312
|
+
continue
|
|
313
|
+
numeric_string_fields.add(key)
|
|
314
|
+
json_key_to_field_def[key] = fb.string_field(field_name, display_name=key)
|
|
315
|
+
update_string_field(json_key_to_field_def[key], value)
|
|
324
316
|
|
|
325
317
|
# Sort the JSON list if requested.
|
|
326
318
|
if sort_field and sort_direction != SortDirection.NONE:
|
|
@@ -340,12 +332,10 @@ class AiHelper:
|
|
|
340
332
|
field_maps: list[dict[str, Any]] = []
|
|
341
333
|
for json_dict in json_list:
|
|
342
334
|
field_map: dict[str, Any] = {}
|
|
343
|
-
for key, field in
|
|
344
|
-
# Watch out for NaN values or other special values in numeric columns.
|
|
335
|
+
for key, field in json_key_to_field_def.items():
|
|
345
336
|
val: Any = json_dict.get(key)
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
val = None
|
|
337
|
+
if key in numeric_string_fields and val is not None and isinstance(val, (int, float)):
|
|
338
|
+
val: str = f"{val:.3f}"
|
|
349
339
|
field_map[field.data_field_name] = val
|
|
350
340
|
field_maps.append(field_map)
|
|
351
341
|
|
|
@@ -354,7 +344,7 @@ class AiHelper:
|
|
|
354
344
|
ElnBaseDataType.EXPERIMENT_DETAIL.data_type_name,
|
|
355
345
|
self.tab_next_entry_order(tab),
|
|
356
346
|
notebook_experiment_tab_id=tab.tab_id,
|
|
357
|
-
field_definition_list=
|
|
347
|
+
field_definition_list=[y for x, y in json_key_to_field_def.items()])
|
|
358
348
|
entry = self.eln_man.add_experiment_entry(self.exp_id, detail_entry)
|
|
359
349
|
records: list[DataRecord] = self.dr_man.add_data_records_with_data(entry.data_type_name, field_maps)
|
|
360
350
|
|
|
@@ -539,6 +529,8 @@ class ToolOfToolsHelper:
|
|
|
539
529
|
# Stuff created by this helper.
|
|
540
530
|
_initialized: bool
|
|
541
531
|
"""Whether a tab for this tool has been initialized."""
|
|
532
|
+
_new_tab: bool
|
|
533
|
+
"""Whether a new tab was created for this tool."""
|
|
542
534
|
tab: ElnExperimentTab
|
|
543
535
|
"""The tab that contains the tool's entries."""
|
|
544
536
|
description_entry: ElnEntryStep | None
|
|
@@ -577,6 +569,15 @@ class ToolOfToolsHelper:
|
|
|
577
569
|
self.eln_man = ElnManager(self.user)
|
|
578
570
|
|
|
579
571
|
self._initialized = False
|
|
572
|
+
self._new_tab = False
|
|
573
|
+
|
|
574
|
+
@property
|
|
575
|
+
def is_initialized(self) -> bool:
|
|
576
|
+
return self._initialized
|
|
577
|
+
|
|
578
|
+
@property
|
|
579
|
+
def is_new_tab(self) -> bool:
|
|
580
|
+
return self._new_tab
|
|
580
581
|
|
|
581
582
|
def initialize_tab(self) -> ElnExperimentTab:
|
|
582
583
|
if self._initialized:
|
|
@@ -629,6 +630,7 @@ class ToolOfToolsHelper:
|
|
|
629
630
|
|
|
630
631
|
# Otherwise, create the tab for the tool progress and results.
|
|
631
632
|
self.tab = self.helper.create_tab(tab_name)
|
|
633
|
+
self._new_tab = True
|
|
632
634
|
|
|
633
635
|
# Create a hidden entry for tracking the progress of the tool.
|
|
634
636
|
field_sets: list[ElnFieldSetInfo] = self.eln_man.get_field_set_info_list()
|
|
@@ -636,16 +638,18 @@ class ToolOfToolsHelper:
|
|
|
636
638
|
x.field_set_name == "Tool of Tools Progress"]
|
|
637
639
|
if not progress_field_set:
|
|
638
640
|
raise SapioException("Unable to locate the field set for the Tool of Tools progress.")
|
|
639
|
-
progress_entry_crit =
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
641
|
+
progress_entry_crit = _ElnEntryCriteria(ElnEntryType.Form, f"{tab_name} Progress",
|
|
642
|
+
ElnBaseDataType.EXPERIMENT_DETAIL.data_type_name, 1,
|
|
643
|
+
notebook_experiment_tab_id=self.tab.tab_id,
|
|
644
|
+
enb_field_set_id=progress_field_set[0].field_set_id,
|
|
645
|
+
is_hidden=True)
|
|
643
646
|
progress_entry = ElnEntryStep(self.helper.protocol,
|
|
644
647
|
self.eln_man.add_experiment_entry(self.exp_id, progress_entry_crit))
|
|
645
648
|
self.progress_entry = progress_entry
|
|
646
649
|
self.progress_record = progress_entry.get_records()[0]
|
|
647
650
|
|
|
648
651
|
# Hide the progress entry.
|
|
652
|
+
# TODO: Remove once we get this working on entry creation.
|
|
649
653
|
form_update_crit = ElnFormEntryUpdateCriteria()
|
|
650
654
|
form_update_crit.is_hidden = True
|
|
651
655
|
self.eln_man.update_experiment_entry(self.exp_id, self.progress_entry.get_id(), form_update_crit)
|
|
@@ -654,26 +658,22 @@ class ToolOfToolsHelper:
|
|
|
654
658
|
# tool started and format the description so that the text isn't too small to read.
|
|
655
659
|
# TODO: Get the UTC offset in seconds from the header once that's being sent.
|
|
656
660
|
now: str = TimeUtil.now_in_format("%Y-%m-%d %H:%M:%S UTC", "UTC")
|
|
657
|
-
|
|
661
|
+
description: str = f"<p>{HtmlFormatter.timestamp(now)}<br>{HtmlFormatter.body(self.description)}</p>"
|
|
662
|
+
text_entry: ElnEntryStep = _ELNStepFactory.create_text_entry(self.helper.protocol, description,
|
|
663
|
+
column_order=0, column_span=2)
|
|
658
664
|
self.description_entry = text_entry
|
|
659
665
|
self.description_record = text_entry.get_records()[0]
|
|
660
666
|
|
|
661
|
-
# Shrink the text entry by one column.
|
|
662
|
-
text_update_crit = ElnTextEntryUpdateCriteria()
|
|
663
|
-
text_update_crit.column_order = 0
|
|
664
|
-
text_update_crit.column_span = 2
|
|
665
|
-
self.eln_man.update_experiment_entry(self.exp_id, self.description_entry.get_id(), text_update_crit)
|
|
666
|
-
|
|
667
667
|
# Create a gauge entry to display the progress.
|
|
668
|
-
gauge_entry: ElnEntryStep =
|
|
669
|
-
|
|
668
|
+
gauge_entry: ElnEntryStep = _ELNStepFactory._create_gauge_chart(self.helper.protocol, progress_entry,
|
|
669
|
+
f"{self.name} Progress", "Progress", "StatusMsg",
|
|
670
|
+
column_order=2, column_span=2, entry_height=250)
|
|
670
671
|
self.progress_gauge_entry = gauge_entry
|
|
671
672
|
|
|
672
673
|
# Make sure the gauge entry isn't too big and stick it to the right of the text entry.
|
|
674
|
+
# TODO: Remove once we get this working on entry creation.
|
|
673
675
|
dash_update_crit = ElnDashboardEntryUpdateCriteria()
|
|
674
676
|
dash_update_crit.entry_height = 250
|
|
675
|
-
dash_update_crit.column_order = 2
|
|
676
|
-
dash_update_crit.column_span = 2
|
|
677
677
|
self.eln_man.update_experiment_entry(self.exp_id, self.progress_gauge_entry.get_id(), dash_update_crit)
|
|
678
678
|
|
|
679
679
|
# Create a results entry if this tool produces result records.
|
|
@@ -768,10 +768,37 @@ class ToolOfToolsHelper:
|
|
|
768
768
|
|
|
769
769
|
return self.helper.create_attachment_entry_from_file(self.tab, entry_name, file_path)
|
|
770
770
|
|
|
771
|
+
|
|
772
|
+
class _ELNStepFactory:
|
|
773
|
+
"""
|
|
774
|
+
Factory that provides simple functions to create a new ELN step under an ELN protocol.
|
|
775
|
+
"""
|
|
776
|
+
@staticmethod
|
|
777
|
+
def create_text_entry(protocol: ElnExperimentProtocol, text_data: str,
|
|
778
|
+
position: ElnEntryPosition | None = None, **kwargs) -> ElnEntryStep:
|
|
779
|
+
"""
|
|
780
|
+
Create a text entry at the end of the protocol, with a initial text specified in the text entry.
|
|
781
|
+
:param protocol: The protocol to create a new step for.
|
|
782
|
+
:param text_data: Must be non-blank. This is what will be displayed. Some HTML format tags can be inserted.
|
|
783
|
+
:param position: The position of the new step. If not specified, the new step will be added at the end.
|
|
784
|
+
:return: The new text entry step.
|
|
785
|
+
"""
|
|
786
|
+
eln_manager, new_entry = _ELNStepFactory._get_entry_creation_criteria(ElnBaseDataType.TEXT_ENTRY_DETAIL.data_type_name,
|
|
787
|
+
protocol, 'Text Entry', ElnEntryType.Text,
|
|
788
|
+
position, **kwargs)
|
|
789
|
+
record = eln_manager.get_data_records_for_entry(protocol.eln_experiment.notebook_experiment_id,
|
|
790
|
+
new_entry.entry_id).result_list[0]
|
|
791
|
+
record.set_field_value(ElnBaseDataType.get_text_entry_data_field_name(), text_data)
|
|
792
|
+
DataMgmtServer.get_data_record_manager(protocol.user).commit_data_records([record])
|
|
793
|
+
ret = ElnEntryStep(protocol, new_entry)
|
|
794
|
+
protocol.invalidate()
|
|
795
|
+
return ret
|
|
796
|
+
|
|
771
797
|
# TODO: Remove this once pylib's gauge chart definition is up to date.
|
|
772
798
|
@staticmethod
|
|
773
799
|
def _create_gauge_chart(protocol: ElnExperimentProtocol, data_source_step: ElnEntryStep, step_name: str,
|
|
774
|
-
field_name: str, status_field: str, group_by_field_name: str = "DataRecordName"
|
|
800
|
+
field_name: str, status_field: str, group_by_field_name: str = "DataRecordName",
|
|
801
|
+
**kwargs) \
|
|
775
802
|
-> ElnEntryStep:
|
|
776
803
|
"""
|
|
777
804
|
Create a gauge chart step in the experiment protocol.
|
|
@@ -790,11 +817,55 @@ class ToolOfToolsHelper:
|
|
|
790
817
|
chart.grouping_type = ChartGroupingType.GROUP_BY_FIELD
|
|
791
818
|
chart.grouping_type_data_type_name = data_type_name
|
|
792
819
|
chart.grouping_type_data_field_name = group_by_field_name
|
|
793
|
-
dashboard, step =
|
|
794
|
-
|
|
820
|
+
dashboard, step = _ELNStepFactory._create_dashboard_step_from_chart(chart, data_source_step, protocol, step_name,
|
|
821
|
+
None, **kwargs)
|
|
795
822
|
protocol.invalidate()
|
|
796
823
|
return step
|
|
797
824
|
|
|
825
|
+
@staticmethod
|
|
826
|
+
def _create_dashboard_step_from_chart(chart: GaugeChartDefinition, data_source_step: ElnEntryStep,
|
|
827
|
+
protocol: ElnExperimentProtocol, step_name: str,
|
|
828
|
+
position: ElnEntryPosition | None = None, **kwargs) -> \
|
|
829
|
+
tuple[DashboardDefinition, ElnEntryStep]:
|
|
830
|
+
dashboard: DashboardDefinition = DashboardDefinition()
|
|
831
|
+
dashboard.chart_definition_list = [chart]
|
|
832
|
+
dashboard.dashboard_scope = DashboardScope.PRIVATE_ELN
|
|
833
|
+
dashboard = DataMgmtServer.get_dashboard_manager(protocol.user).store_dashboard_definition(dashboard)
|
|
834
|
+
eln_manager, new_entry = _ELNStepFactory._get_entry_creation_criteria("", protocol, step_name,
|
|
835
|
+
ElnEntryType.Dashboard, position,
|
|
836
|
+
**kwargs)
|
|
837
|
+
# noinspection PyTypeChecker
|
|
838
|
+
update_criteria = ElnDashboardEntryUpdateCriteria()
|
|
839
|
+
update_criteria.dashboard_guid = dashboard.dashboard_guid
|
|
840
|
+
update_criteria.data_source_entry_id = data_source_step.get_id()
|
|
841
|
+
update_criteria.entry_height = 500
|
|
842
|
+
eln_manager.update_experiment_entry(protocol.eln_experiment.notebook_experiment_id, new_entry.entry_id,
|
|
843
|
+
update_criteria)
|
|
844
|
+
step = ElnEntryStep(protocol, new_entry)
|
|
845
|
+
return dashboard, step
|
|
846
|
+
|
|
847
|
+
@staticmethod
|
|
848
|
+
def _get_entry_creation_criteria(data_type_name: str | None, protocol: ElnExperimentProtocol,
|
|
849
|
+
step_name: str, entry_type: ElnEntryType, position: ElnEntryPosition | None = None,
|
|
850
|
+
**kwargs):
|
|
851
|
+
tab_id: int | None = None
|
|
852
|
+
order: int | None = None
|
|
853
|
+
if position:
|
|
854
|
+
tab_id = position.tab_id
|
|
855
|
+
order = position.order
|
|
856
|
+
# noinspection PyTypeChecker
|
|
857
|
+
last_step: ElnEntryStep = protocol.get_sorted_step_list()[-1]
|
|
858
|
+
if tab_id is None:
|
|
859
|
+
tab_id = last_step.eln_entry.notebook_experiment_tab_id
|
|
860
|
+
if order is None:
|
|
861
|
+
order = last_step.eln_entry.order + 1
|
|
862
|
+
eln_manager = DataMgmtServer.get_eln_manager(protocol.user)
|
|
863
|
+
entry_criteria = _ElnEntryCriteria(entry_type, step_name, data_type_name, order,
|
|
864
|
+
notebook_experiment_tab_id=tab_id, **kwargs)
|
|
865
|
+
new_entry: ExperimentEntry = eln_manager.add_experiment_entry(protocol.eln_experiment.notebook_experiment_id,
|
|
866
|
+
entry_criteria)
|
|
867
|
+
return eln_manager, new_entry
|
|
868
|
+
|
|
798
869
|
|
|
799
870
|
# TODO: Using this to set the new status field setting.
|
|
800
871
|
class _GaugeChartDefinition(GaugeChartDefinition):
|
|
@@ -807,3 +878,40 @@ class _GaugeChartDefinition(GaugeChartDefinition):
|
|
|
807
878
|
"dataFieldName": self.status_field
|
|
808
879
|
}
|
|
809
880
|
return result
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
class _ElnEntryCriteria(ElnEntryCriteria):
|
|
884
|
+
is_hidden: bool | None
|
|
885
|
+
entry_height: int | None
|
|
886
|
+
description: str | None
|
|
887
|
+
is_initialization_required: bool | None
|
|
888
|
+
collapse_entry: bool | None
|
|
889
|
+
entry_status: ExperimentEntryStatus | None
|
|
890
|
+
template_item_fulfilled_timestamp: int | None
|
|
891
|
+
|
|
892
|
+
def __init__(self, entry_type: ElnEntryType, entry_name: str | None, data_type_name: str | None, order: int,
|
|
893
|
+
is_hidden: bool | None = None, entry_height: int | None = None, description: str | None = None,
|
|
894
|
+
is_initialization_required: bool | None = None, collapse_entry: bool | None = None,
|
|
895
|
+
entry_status: ExperimentEntryStatus | None = None, template_item_fulfilled_timestamp: int | None = None,
|
|
896
|
+
**kwargs):
|
|
897
|
+
super().__init__(entry_type, entry_name, data_type_name, order, **kwargs)
|
|
898
|
+
self.is_hidden = is_hidden
|
|
899
|
+
self.entry_height = entry_height
|
|
900
|
+
self.description = description
|
|
901
|
+
self.is_initialization_required = is_initialization_required
|
|
902
|
+
self.collapse_entry = collapse_entry
|
|
903
|
+
self.entry_status = entry_status
|
|
904
|
+
self.template_item_fulfilled_timestamp = template_item_fulfilled_timestamp
|
|
905
|
+
|
|
906
|
+
def to_json(self) -> dict[str, Any]:
|
|
907
|
+
ret: dict[str, Any] = super().to_json()
|
|
908
|
+
ret.update({
|
|
909
|
+
"hidden": self.is_hidden,
|
|
910
|
+
"entryHeight": self.entry_height,
|
|
911
|
+
"description": self.description,
|
|
912
|
+
"initializationRequired": self.is_initialization_required,
|
|
913
|
+
"collapsed": self.collapse_entry,
|
|
914
|
+
"entryStatus": self.entry_status,
|
|
915
|
+
"templateItemFulfilledTimestamp": self.template_item_fulfilled_timestamp
|
|
916
|
+
})
|
|
917
|
+
return ret
|
|
@@ -1815,7 +1815,8 @@ class CallbackUtil:
|
|
|
1815
1815
|
return response
|
|
1816
1816
|
|
|
1817
1817
|
def request_file(self, title: str, exts: Iterable[str] | None = None,
|
|
1818
|
-
show_image_editor: bool = False, show_camera_button: bool = False
|
|
1818
|
+
show_image_editor: bool = False, show_camera_button: bool = False,
|
|
1819
|
+
*, enforce_file_extensions: bool = True) -> tuple[str, bytes]:
|
|
1819
1820
|
"""
|
|
1820
1821
|
Request a single file from the user.
|
|
1821
1822
|
|
|
@@ -1825,6 +1826,8 @@ class CallbackUtil:
|
|
|
1825
1826
|
:param show_image_editor: Whether the user will see an image editor when image is uploaded in this file prompt.
|
|
1826
1827
|
:param show_camera_button: Whether the user will be able to use camera to take a picture as an upload request,
|
|
1827
1828
|
rather than selecting an existing file.
|
|
1829
|
+
:param enforce_file_extensions: If true, then the file extensions provided in the exts parameter will be
|
|
1830
|
+
enforced. If false, then the user may upload any file type.
|
|
1828
1831
|
:return: The file name and bytes of the uploaded file.
|
|
1829
1832
|
"""
|
|
1830
1833
|
# If no extensions were provided, use an empty list for the extensions instead.
|
|
@@ -1844,11 +1847,12 @@ class CallbackUtil:
|
|
|
1844
1847
|
file_path: str = self.__send_dialog(request, self.callback.show_file_dialog, data_sink=do_consume)
|
|
1845
1848
|
|
|
1846
1849
|
# Verify that each of the file given matches the expected extension(s).
|
|
1847
|
-
self.__verify_file(file_path, sink.data, exts)
|
|
1850
|
+
self.__verify_file(file_path, sink.data, exts if enforce_file_extensions else None)
|
|
1848
1851
|
return file_path, sink.data
|
|
1849
1852
|
|
|
1850
1853
|
def request_files(self, title: str, exts: Iterable[str] | None = None,
|
|
1851
|
-
show_image_editor: bool = False, show_camera_button: bool = False
|
|
1854
|
+
show_image_editor: bool = False, show_camera_button: bool = False,
|
|
1855
|
+
*, enforce_file_extensions: bool = True) -> dict[str, bytes]:
|
|
1852
1856
|
"""
|
|
1853
1857
|
Request multiple files from the user.
|
|
1854
1858
|
|
|
@@ -1858,6 +1862,8 @@ class CallbackUtil:
|
|
|
1858
1862
|
:param show_image_editor: Whether the user will see an image editor when image is uploaded in this file prompt.
|
|
1859
1863
|
:param show_camera_button: Whether the user will be able to use camera to take a picture as an upload request,
|
|
1860
1864
|
rather than selecting an existing file.
|
|
1865
|
+
:param enforce_file_extensions: If true, then the file extensions provided in the exts parameter will be
|
|
1866
|
+
enforced. If false, then the user may upload any file type.
|
|
1861
1867
|
:return: A dictionary of file name to file bytes for each file the user uploaded.
|
|
1862
1868
|
"""
|
|
1863
1869
|
# If no extensions were provided, use an empty list for the extensions instead.
|
|
@@ -1873,7 +1879,7 @@ class CallbackUtil:
|
|
|
1873
1879
|
for file_path in file_paths:
|
|
1874
1880
|
sink = InMemoryRecordDataSink(self.user)
|
|
1875
1881
|
sink.consume_client_callback_file_path_data(file_path)
|
|
1876
|
-
self.__verify_file(file_path, sink.data, exts)
|
|
1882
|
+
self.__verify_file(file_path, sink.data, exts if enforce_file_extensions else None)
|
|
1877
1883
|
ret_dict.update({file_path: sink.data})
|
|
1878
1884
|
|
|
1879
1885
|
return ret_dict
|
|
@@ -1890,16 +1896,17 @@ class CallbackUtil:
|
|
|
1890
1896
|
"""
|
|
1891
1897
|
if file_path is None or len(file_path) == 0 or file_bytes is None or len(file_bytes) == 0:
|
|
1892
1898
|
raise SapioUserErrorException("Empty file provided or file unable to be read.")
|
|
1893
|
-
if allowed_extensions:
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1899
|
+
if not allowed_extensions:
|
|
1900
|
+
return
|
|
1901
|
+
matches: bool = False
|
|
1902
|
+
for ext in allowed_extensions:
|
|
1903
|
+
# FR-47690: Changed to a case-insensitive match.
|
|
1904
|
+
if file_path.casefold().endswith("." + ext.lstrip(".").casefold()):
|
|
1905
|
+
matches = True
|
|
1906
|
+
break
|
|
1907
|
+
if not matches:
|
|
1908
|
+
raise SapioUserErrorException("Unsupported file type. Expecting the following extension(s): "
|
|
1909
|
+
+ (",".join(allowed_extensions)))
|
|
1903
1910
|
|
|
1904
1911
|
def write_file(self, file_name: str, file_data: str | bytes) -> None:
|
|
1905
1912
|
"""
|