sapiopycommons 2025.5.20a539__tar.gz → 2025.5.30a548__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 (89) hide show
  1. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/PKG-INFO +1 -1
  2. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/pyproject.toml +1 -1
  3. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/ai/tool_of_tools.py +235 -127
  4. sapiopycommons-2025.5.30a548/src/sapiopycommons/general/html_formatter.py +456 -0
  5. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/general/sapio_links.py +12 -4
  6. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/recordmodel/record_handler.py +354 -344
  7. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/.gitignore +0 -0
  8. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/LICENSE +0 -0
  9. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/README.md +0 -0
  10. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/__init__.py +0 -0
  11. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/ai/__init__.py +0 -0
  12. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/callbacks/__init__.py +0 -0
  13. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/callbacks/callback_util.py +0 -0
  14. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/callbacks/field_builder.py +0 -0
  15. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/chem/IndigoMolecules.py +0 -0
  16. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/chem/Molecules.py +0 -0
  17. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/chem/__init__.py +0 -0
  18. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/customreport/__init__.py +0 -0
  19. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/customreport/auto_pagers.py +0 -0
  20. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/customreport/column_builder.py +0 -0
  21. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/customreport/custom_report_builder.py +0 -0
  22. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/customreport/term_builder.py +0 -0
  23. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/datatype/__init__.py +0 -0
  24. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/datatype/attachment_util.py +0 -0
  25. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/datatype/data_fields.py +0 -0
  26. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/datatype/pseudo_data_types.py +0 -0
  27. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/eln/__init__.py +0 -0
  28. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/eln/experiment_cache.py +0 -0
  29. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/eln/experiment_handler.py +0 -0
  30. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/eln/experiment_report_util.py +0 -0
  31. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/eln/experiment_step_factory.py +0 -0
  32. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/eln/experiment_tags.py +0 -0
  33. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/eln/plate_designer.py +0 -0
  34. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/eln/step_creation.py +0 -0
  35. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/files/__init__.py +0 -0
  36. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/files/complex_data_loader.py +0 -0
  37. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/files/file_bridge.py +0 -0
  38. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/files/file_bridge_handler.py +0 -0
  39. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/files/file_data_handler.py +0 -0
  40. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/files/file_util.py +0 -0
  41. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/files/file_validator.py +0 -0
  42. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/files/file_writer.py +0 -0
  43. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/flowcyto/flow_cyto.py +0 -0
  44. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/flowcyto/flowcyto_data.py +0 -0
  45. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/general/__init__.py +0 -0
  46. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/general/accession_service.py +0 -0
  47. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/general/aliases.py +0 -0
  48. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/general/audit_log.py +0 -0
  49. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/general/custom_report_util.py +0 -0
  50. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/general/data_structure_util.py +0 -0
  51. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/general/directive_util.py +0 -0
  52. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/general/exceptions.py +0 -0
  53. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/general/popup_util.py +0 -0
  54. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/general/storage_util.py +0 -0
  55. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/general/time_util.py +0 -0
  56. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/multimodal/multimodal.py +0 -0
  57. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/multimodal/multimodal_data.py +0 -0
  58. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/processtracking/__init__.py +0 -0
  59. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/processtracking/custom_workflow_handler.py +0 -0
  60. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/processtracking/endpoints.py +0 -0
  61. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/recordmodel/__init__.py +0 -0
  62. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/rules/__init__.py +0 -0
  63. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/rules/eln_rule_handler.py +0 -0
  64. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/rules/on_save_rule_handler.py +0 -0
  65. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/samples/aliquot.py +0 -0
  66. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/sftpconnect/__init__.py +0 -0
  67. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/sftpconnect/sftp_builder.py +0 -0
  68. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/webhook/__init__.py +0 -0
  69. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/webhook/webhook_context.py +0 -0
  70. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/webhook/webhook_handlers.py +0 -0
  71. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/src/sapiopycommons/webhook/webservice_handlers.py +0 -0
  72. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/AF-A0A009IHW8-F1-model_v4.cif +0 -0
  73. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/_do_not_add_init_py_here +0 -0
  74. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/accession_test.py +0 -0
  75. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/aliquot_test.py +0 -0
  76. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/bio_reg_test.py +0 -0
  77. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/chem_test.py +0 -0
  78. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/chem_test_curation_queue.py +0 -0
  79. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/curation_queue_test.sdf +0 -0
  80. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/data_type_models.py +0 -0
  81. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/flowcyto/101_DEN084Y5_15_E01_008_clean.fcs +0 -0
  82. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/flowcyto/101_DEN084Y5_15_E03_009_clean.fcs +0 -0
  83. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/flowcyto/101_DEN084Y5_15_E05_010_clean.fcs +0 -0
  84. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/flowcyto/8_color_ICS.wsp +0 -0
  85. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/flowcyto/COVID19_W_001_O.fcs +0 -0
  86. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/flowcyto_test.py +0 -0
  87. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/kappa.chains.fasta +0 -0
  88. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/mafft_test.py +0 -0
  89. {sapiopycommons-2025.5.20a539 → sapiopycommons-2025.5.30a548}/tests/test.gb +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2025.5.20a539
3
+ Version: 2025.5.30a548
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.05.20a539'
7
+ version='2025.05.30a548'
8
8
  authors = [
9
9
  { name="Jonathan Steck", email="jsteck@sapiosciences.com" },
10
10
  { name="Yechen Qiao", email="yqiao@sapiosciences.com" },
@@ -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, ElnTextEntryUpdateCriteria
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 call_endpoint(self, url: str, payload: Any, tab_prefix: str = "") -> Response:
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
- fields: list[AbstractVeloxFieldDefinition] = []
313
- fields_by_name: dict[str, AbstractVeloxFieldDefinition] = {}
314
- for key, value in json_list[0].items():
315
- field_name: str = key.replace(" ", "_")
316
- if isinstance(value, str):
317
- field = fb.string_field(field_name, display_name=key)
318
- fields.append(field)
319
- fields_by_name[key] = field
320
- elif isinstance(value, (int, float)):
321
- field = fb.double_field(field_name, display_name=key, precision=3)
322
- fields.append(field)
323
- fields_by_name[key] = field
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 fields_by_name.items():
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 (field.data_field_type == FieldType.DOUBLE
347
- and (not isinstance(val, (int, float))) or (isinstance(val, float) and math.isnan(val))):
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=fields)
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 = ElnEntryCriteria(ElnEntryType.Form, f"{tab_name} Progress",
640
- ElnBaseDataType.EXPERIMENT_DETAIL.data_type_name, 1,
641
- notebook_experiment_tab_id=self.tab.tab_id,
642
- enb_field_set_id=progress_field_set[0].field_set_id)
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
- text_entry = ElnEntryStep(self.helper.protocol, self.helper.create_text_entry(self.tab, now, self.description))
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 = self._create_gauge_chart(self.helper.protocol, progress_entry,
669
- f"{self.name} Progress", "Progress", "StatusMsg")
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 = ELNStepFactory._create_dashboard_step_from_chart(chart, data_source_step, protocol, step_name,
794
- None)
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