sapiopycommons 2025.2.25a448__tar.gz → 2025.2.25a450__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 (85) hide show
  1. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/PKG-INFO +1 -1
  2. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/pyproject.toml +1 -1
  3. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/ai/tool_of_tools.py +120 -66
  4. sapiopycommons-2025.2.25a448/src/sapiopycommons/ai/biopython_helper.py +0 -639
  5. sapiopycommons-2025.2.25a448/src/sapiopycommons/ai/rdkit_helper.py +0 -82
  6. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/.gitignore +0 -0
  7. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/LICENSE +0 -0
  8. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/README.md +0 -0
  9. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/__init__.py +0 -0
  10. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/ai/__init__.py +0 -0
  11. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/callbacks/__init__.py +0 -0
  12. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/callbacks/callback_util.py +0 -0
  13. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/callbacks/field_builder.py +0 -0
  14. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/chem/IndigoMolecules.py +0 -0
  15. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/chem/Molecules.py +0 -0
  16. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/chem/__init__.py +0 -0
  17. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/customreport/__init__.py +0 -0
  18. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/customreport/auto_pagers.py +0 -0
  19. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/customreport/column_builder.py +0 -0
  20. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/customreport/custom_report_builder.py +0 -0
  21. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/customreport/term_builder.py +0 -0
  22. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/datatype/__init__.py +0 -0
  23. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/datatype/attachment_util.py +0 -0
  24. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/datatype/data_fields.py +0 -0
  25. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/datatype/pseudo_data_types.py +0 -0
  26. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/eln/__init__.py +0 -0
  27. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/eln/experiment_handler.py +0 -0
  28. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/eln/experiment_report_util.py +0 -0
  29. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/eln/plate_designer.py +0 -0
  30. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/files/__init__.py +0 -0
  31. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/files/complex_data_loader.py +0 -0
  32. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/files/file_bridge.py +0 -0
  33. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/files/file_bridge_handler.py +0 -0
  34. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/files/file_data_handler.py +0 -0
  35. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/files/file_util.py +0 -0
  36. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/files/file_validator.py +0 -0
  37. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/files/file_writer.py +0 -0
  38. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/flowcyto/flow_cyto.py +0 -0
  39. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/flowcyto/flowcyto_data.py +0 -0
  40. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/general/__init__.py +0 -0
  41. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/general/accession_service.py +0 -0
  42. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/general/aliases.py +0 -0
  43. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/general/audit_log.py +0 -0
  44. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/general/custom_report_util.py +0 -0
  45. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/general/directive_util.py +0 -0
  46. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/general/exceptions.py +0 -0
  47. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/general/popup_util.py +0 -0
  48. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/general/sapio_links.py +0 -0
  49. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/general/storage_util.py +0 -0
  50. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/general/time_util.py +0 -0
  51. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/multimodal/multimodal.py +0 -0
  52. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/multimodal/multimodal_data.py +0 -0
  53. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/processtracking/__init__.py +0 -0
  54. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/processtracking/custom_workflow_handler.py +0 -0
  55. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/processtracking/endpoints.py +0 -0
  56. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/recordmodel/__init__.py +0 -0
  57. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/recordmodel/record_handler.py +0 -0
  58. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/rules/__init__.py +0 -0
  59. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/rules/eln_rule_handler.py +0 -0
  60. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/rules/on_save_rule_handler.py +0 -0
  61. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/samples/aliquot.py +0 -0
  62. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/sftpconnect/__init__.py +0 -0
  63. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/sftpconnect/sftp_builder.py +0 -0
  64. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/webhook/__init__.py +0 -0
  65. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/webhook/webhook_context.py +0 -0
  66. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/webhook/webhook_handlers.py +0 -0
  67. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/src/sapiopycommons/webhook/webservice_handlers.py +0 -0
  68. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/AF-A0A009IHW8-F1-model_v4.cif +0 -0
  69. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/_do_not_add_init_py_here +0 -0
  70. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/accession_test.py +0 -0
  71. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/aliquot_test.py +0 -0
  72. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/bio_reg_test.py +0 -0
  73. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/chem_test.py +0 -0
  74. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/chem_test_curation_queue.py +0 -0
  75. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/curation_queue_test.sdf +0 -0
  76. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/data_type_models.py +0 -0
  77. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/flowcyto/101_DEN084Y5_15_E01_008_clean.fcs +0 -0
  78. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/flowcyto/101_DEN084Y5_15_E03_009_clean.fcs +0 -0
  79. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/flowcyto/101_DEN084Y5_15_E05_010_clean.fcs +0 -0
  80. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/flowcyto/8_color_ICS.wsp +0 -0
  81. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/flowcyto/COVID19_W_001_O.fcs +0 -0
  82. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/flowcyto_test.py +0 -0
  83. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/kappa.chains.fasta +0 -0
  84. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/mafft_test.py +0 -0
  85. {sapiopycommons-2025.2.25a448 → sapiopycommons-2025.2.25a450}/tests/test.gb +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2025.2.25a448
3
+ Version: 2025.2.25a450
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.02.25a448'
7
+ version='2025.02.25a450'
8
8
  authors = [
9
9
  { name="Jonathan Steck", email="jsteck@sapiosciences.com" },
10
10
  { name="Yechen Qiao", email="yqiao@sapiosciences.com" },
@@ -2,19 +2,20 @@ import base64
2
2
  import io
3
3
  import math
4
4
  import re
5
- from typing import Final, Mapping, Any, cast
5
+ from typing import Final, Mapping, Any
6
6
 
7
7
  import requests
8
8
  from pandas import DataFrame
9
9
  from requests import Response
10
+ from sapiopylib.rest.DataMgmtService import DataMgmtServer
10
11
  from sapiopylib.rest.DataRecordManagerService import DataRecordManager
11
12
  from sapiopylib.rest.DataTypeService import DataTypeManager
12
13
  from sapiopylib.rest.ELNService import ElnManager
13
14
  from sapiopylib.rest.User import SapioUser
14
15
  from sapiopylib.rest.pojo.DataRecord import DataRecord
15
16
  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
17
+ from sapiopylib.rest.pojo.chartdata.DashboardDefinition import GaugeChartDefinition, DashboardDefinition
18
+ from sapiopylib.rest.pojo.chartdata.DashboardEnums import ChartGroupingType, ChartOperationType, DashboardScope
18
19
  from sapiopylib.rest.pojo.chartdata.DashboardSeries import GaugeChartSeries
19
20
  from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
20
21
  from sapiopylib.rest.pojo.datatype.DataTypeLayout import DataTypeLayout, TableLayout
@@ -24,7 +25,7 @@ from sapiopylib.rest.pojo.eln.ElnEntryPosition import ElnEntryPosition
24
25
  from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
25
26
  from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry
26
27
  from sapiopylib.rest.pojo.eln.ExperimentEntryCriteria import ElnEntryCriteria, ElnFormEntryUpdateCriteria, \
27
- ElnDashboardEntryUpdateCriteria, ElnTextEntryUpdateCriteria
28
+ ElnDashboardEntryUpdateCriteria
28
29
  from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnEntryType, ElnBaseDataType
29
30
  from sapiopylib.rest.pojo.eln.eln_headings import ElnExperimentTabAddCriteria, ElnExperimentTab
30
31
  from sapiopylib.rest.pojo.eln.field_set import ElnFieldSetInfo
@@ -325,57 +326,48 @@ class AiHelper:
325
326
  if not json_list:
326
327
  return None
327
328
 
328
- def init_string_field(k: str, v: Any, n: str) -> VeloxStringFieldDefinition:
329
+ def update_string_field(f: AbstractVeloxFieldDefinition, v: Any) -> None:
329
330
  """
330
- Initialize a string field.
331
+ Update the max length of the string field and whether it is a link-out field depending on the length and
332
+ form of the given value.
331
333
 
332
- :param k: The JSON key that the field value is being pulled from. Doubles as the display name.
333
- :param v: A particular value of the field.
334
- :param n: The unique name of the field.
334
+ :param f: The definition of the string field.
335
+ :param v: A field value that will be present for this field.
335
336
  """
336
- link_out: dict[str, str] = {}
337
- if isinstance(v, str):
338
- if v.startswith("https://") or v.startswith("http://"):
339
- link_out["Link"] = "[[LINK_OUT]]"
340
- return fb.string_field(n, display_name=k, link_out=link_out)
341
-
342
- def update_field_length(k: str, v: Any, lengths: dict[str, int]) -> None:
343
- """
344
- Update the max length of a string field.
345
-
346
- :param k: The JSON key that the field value is being pulled from.
347
- :param v: The field value.
348
- :param lengths: The dictionary of field lengths.
349
- """
350
- lengths[k] = max(lengths.get(k, 100), len(str(v)) if v is not None else 0)
337
+ if not isinstance(f, VeloxStringFieldDefinition) or v is None:
338
+ return
339
+ sv = str(v)
340
+ f.max_length = max(f.max_length, len(sv))
341
+ if not f.link_out and sv.startswith("http://") or sv.startswith("https://"):
342
+ link_out, link_out_url = FieldBuilder._convert_link_out({"Link": "[[LINK_OUT]]"})
343
+ f.link_out = link_out
344
+ f.link_out_url = link_out_url
351
345
 
352
346
  # Determine which fields in the JSON can be used to create field definitions.
353
347
  fb = FieldBuilder()
354
348
  json_key_to_field_def: dict[str, AbstractVeloxFieldDefinition] = {}
355
- json_key_to_field_name: dict[str, str] = {}
356
- json_key_to_string_length: dict[str, int] = {}
357
349
  numeric_string_fields: set[str] = set()
358
350
  for values in json_list:
359
351
  for key, value in values.items():
360
- # The field name is the JSON key name, but with spaces and dashes replaced by underscores and with a leading
361
- # underscore added if the field name starts with a number.
362
- if key not in json_key_to_field_name:
363
- field_name: str = key.strip()
364
- if " " in field_name:
365
- field_name = field_name.replace(" ", "_")
366
- if "-" in field_name:
367
- field_name = field_name.replace("-", "_")
368
- if field_name[0].isnumeric():
369
- field_name = "_" + field_name
370
- json_key_to_field_name[key] = field_name
371
- else:
372
- field_name = json_key_to_field_name[key]
352
+ # Skip null values, since we can't know what type they're meant to represent.
353
+ if value is None:
354
+ continue
355
+
356
+ # The field name is the JSON key name, but with spaces and dashes replaced by underscores and with a
357
+ # leading underscore added if the field name starts with a number.
358
+ field_name: str = key.strip()
359
+ if " " in field_name:
360
+ field_name = field_name.replace(" ", "_")
361
+ if "-" in field_name:
362
+ field_name = field_name.replace("-", "_")
363
+ if field_name[0].isnumeric():
364
+ field_name = "_" + field_name
373
365
 
374
366
  # If this is the first time this key is being encountered, create a field for it.
375
367
  if key not in json_key_to_field_def:
376
368
  if isinstance(value, str):
377
- json_key_to_field_def[key] = init_string_field(key, value, field_name)
378
- update_field_length(key, value, json_key_to_string_length)
369
+ json_key_to_field_def[key] = fb.string_field(field_name, display_name=key)
370
+ update_string_field(json_key_to_field_def[key], value)
379
371
  elif isinstance(value, bool):
380
372
  json_key_to_field_def[key] = fb.boolean_field(field_name, display_name=key)
381
373
  elif isinstance(value, (int, float)):
@@ -388,25 +380,21 @@ class AiHelper:
388
380
  # Strings can be anything, so we don't need to check the value type.
389
381
  if field_type == FieldType.STRING:
390
382
  # We still need to make sure the lengths are fine.
391
- update_field_length(key, value, json_key_to_string_length)
383
+ update_string_field(json_key_to_field_def[key], value)
392
384
  continue
393
385
  # Boolean values can only be booleans.
394
386
  if field_type == FieldType.BOOLEAN and isinstance(value, bool):
395
387
  continue
396
388
  # Integers and floats both fit in DOUBLE fields, but floats can't be NaN or infinity.
397
- if field_type == FieldType.DOUBLE and not isinstance(value, bool):
398
- if isinstance(value, int):
389
+ if field_type == FieldType.DOUBLE:
390
+ # Booleans count as ints for isinstance, so make sure that true integers continue but bools don't.
391
+ if isinstance(value, int) and not isinstance(value, bool):
399
392
  continue
400
393
  if isinstance(value, float) and not math.isnan(value) and not math.isinf(value):
401
394
  continue
402
395
  numeric_string_fields.add(key)
403
- json_key_to_field_def[key] = init_string_field(key, value, field_name)
404
- update_field_length(key, value, json_key_to_string_length)
405
-
406
- # Update the max length of each string field.
407
- for key, value in json_key_to_string_length.items():
408
- field = cast(VeloxStringFieldDefinition, json_key_to_field_def[key])
409
- field.max_length = value
396
+ json_key_to_field_def[key] = fb.string_field(field_name, display_name=key)
397
+ update_string_field(json_key_to_field_def[key], value)
410
398
 
411
399
  # Sort the JSON list if requested.
412
400
  if sort_field and sort_direction != SortDirection.NONE:
@@ -428,7 +416,7 @@ class AiHelper:
428
416
  field_map: dict[str, Any] = {}
429
417
  for key, field in json_key_to_field_def.items():
430
418
  val: Any = json_dict.get(key)
431
- if key in numeric_string_fields and val is not None and not isinstance(val, str):
419
+ if key in numeric_string_fields and val is not None and isinstance(val, (int, float)):
432
420
  val: str = f"{val:.3f}"
433
421
  field_map[field.data_field_name] = val
434
422
  field_maps.append(field_map)
@@ -738,26 +726,21 @@ class ToolOfToolsHelper:
738
726
  # tool started and format the description so that the text isn't too small to read.
739
727
  # TODO: Get the UTC offset in seconds from the header once that's being sent.
740
728
  now: str = TimeUtil.now_in_format("%Y-%m-%d %H:%M:%S UTC", "UTC")
741
- text_entry = ElnEntryStep(self.helper.protocol, self.helper.create_text_entry(self.tab, now, self.description))
729
+ description: str = f"<p>{HtmlFormatter.timestamp(now)}<br>{HtmlFormatter.body(self.description)}</p>"
730
+ text_entry: ElnEntryStep = _ELNStepFactory.create_text_entry(self.helper.protocol, description,
731
+ column_order=0, column_span=2)
742
732
  self.description_entry = text_entry
743
733
  self.description_record = text_entry.get_records()[0]
744
734
 
745
- # Shrink the text entry by one column.
746
- text_update_crit = ElnTextEntryUpdateCriteria()
747
- text_update_crit.column_order = 0
748
- text_update_crit.column_span = 2
749
- self.eln_man.update_experiment_entry(self.exp_id, self.description_entry.get_id(), text_update_crit)
750
-
751
735
  # Create a gauge entry to display the progress.
752
- gauge_entry: ElnEntryStep = self._create_gauge_chart(self.helper.protocol, progress_entry,
753
- f"{self.name} Progress", "Progress", "StatusMsg")
736
+ gauge_entry: ElnEntryStep = _ELNStepFactory._create_gauge_chart(self.helper.protocol, progress_entry,
737
+ f"{self.name} Progress", "Progress", "StatusMsg",
738
+ column_order=2, column_span=2)
754
739
  self.progress_gauge_entry = gauge_entry
755
740
 
756
741
  # Make sure the gauge entry isn't too big and stick it to the right of the text entry.
757
742
  dash_update_crit = ElnDashboardEntryUpdateCriteria()
758
743
  dash_update_crit.entry_height = 250
759
- dash_update_crit.column_order = 2
760
- dash_update_crit.column_span = 2
761
744
  self.eln_man.update_experiment_entry(self.exp_id, self.progress_gauge_entry.get_id(), dash_update_crit)
762
745
 
763
746
  # Create a results entry if this tool produces result records.
@@ -852,10 +835,37 @@ class ToolOfToolsHelper:
852
835
 
853
836
  return self.helper.create_attachment_entry_from_file(self.tab, entry_name, file_path)
854
837
 
838
+
839
+ class _ELNStepFactory:
840
+ """
841
+ Factory that provides simple functions to create a new ELN step under an ELN protocol.
842
+ """
843
+ @staticmethod
844
+ def create_text_entry(protocol: ElnExperimentProtocol, text_data: str,
845
+ position: ElnEntryPosition | None = None, **kwargs) -> ElnEntryStep:
846
+ """
847
+ Create a text entry at the end of the protocol, with a initial text specified in the text entry.
848
+ :param protocol: The protocol to create a new step for.
849
+ :param text_data: Must be non-blank. This is what will be displayed. Some HTML format tags can be inserted.
850
+ :param position: The position of the new step. If not specified, the new step will be added at the end.
851
+ :return: The new text entry step.
852
+ """
853
+ eln_manager, new_entry = _ELNStepFactory._get_entry_creation_criteria(ElnBaseDataType.TEXT_ENTRY_DETAIL.data_type_name,
854
+ protocol, 'Text Entry', ElnEntryType.Text,
855
+ position, **kwargs)
856
+ record = eln_manager.get_data_records_for_entry(protocol.eln_experiment.notebook_experiment_id,
857
+ new_entry.entry_id).result_list[0]
858
+ record.set_field_value(ElnBaseDataType.get_text_entry_data_field_name(), text_data)
859
+ DataMgmtServer.get_data_record_manager(protocol.user).commit_data_records([record])
860
+ ret = ElnEntryStep(protocol, new_entry)
861
+ protocol.invalidate()
862
+ return ret
863
+
855
864
  # TODO: Remove this once pylib's gauge chart definition is up to date.
856
865
  @staticmethod
857
866
  def _create_gauge_chart(protocol: ElnExperimentProtocol, data_source_step: ElnEntryStep, step_name: str,
858
- field_name: str, status_field: str, group_by_field_name: str = "DataRecordName") \
867
+ field_name: str, status_field: str, group_by_field_name: str = "DataRecordName",
868
+ **kwargs) \
859
869
  -> ElnEntryStep:
860
870
  """
861
871
  Create a gauge chart step in the experiment protocol.
@@ -874,11 +884,55 @@ class ToolOfToolsHelper:
874
884
  chart.grouping_type = ChartGroupingType.GROUP_BY_FIELD
875
885
  chart.grouping_type_data_type_name = data_type_name
876
886
  chart.grouping_type_data_field_name = group_by_field_name
877
- dashboard, step = ELNStepFactory._create_dashboard_step_from_chart(chart, data_source_step, protocol, step_name,
878
- None)
887
+ dashboard, step = _ELNStepFactory._create_dashboard_step_from_chart(chart, data_source_step, protocol, step_name,
888
+ None, **kwargs)
879
889
  protocol.invalidate()
880
890
  return step
881
891
 
892
+ @staticmethod
893
+ def _create_dashboard_step_from_chart(chart: GaugeChartDefinition, data_source_step: ElnEntryStep,
894
+ protocol: ElnExperimentProtocol, step_name: str,
895
+ position: ElnEntryPosition | None = None, **kwargs) -> \
896
+ tuple[DashboardDefinition, ElnEntryStep]:
897
+ dashboard: DashboardDefinition = DashboardDefinition()
898
+ dashboard.chart_definition_list = [chart]
899
+ dashboard.dashboard_scope = DashboardScope.PRIVATE_ELN
900
+ dashboard = DataMgmtServer.get_dashboard_manager(protocol.user).store_dashboard_definition(dashboard)
901
+ eln_manager, new_entry = _ELNStepFactory._get_entry_creation_criteria("", protocol, step_name,
902
+ ElnEntryType.Dashboard, position,
903
+ **kwargs)
904
+ # noinspection PyTypeChecker
905
+ update_criteria = ElnDashboardEntryUpdateCriteria()
906
+ update_criteria.dashboard_guid = dashboard.dashboard_guid
907
+ update_criteria.data_source_entry_id = data_source_step.get_id()
908
+ update_criteria.entry_height = 500
909
+ eln_manager.update_experiment_entry(protocol.eln_experiment.notebook_experiment_id, new_entry.entry_id,
910
+ update_criteria)
911
+ step = ElnEntryStep(protocol, new_entry)
912
+ return dashboard, step
913
+
914
+ @staticmethod
915
+ def _get_entry_creation_criteria(data_type_name: str | None, protocol: ElnExperimentProtocol,
916
+ step_name: str, entry_type: ElnEntryType, position: ElnEntryPosition | None = None,
917
+ **kwargs):
918
+ tab_id: int | None = None
919
+ order: int | None = None
920
+ if position:
921
+ tab_id = position.tab_id
922
+ order = position.order
923
+ # noinspection PyTypeChecker
924
+ last_step: ElnEntryStep = protocol.get_sorted_step_list()[-1]
925
+ if tab_id is None:
926
+ tab_id = last_step.eln_entry.notebook_experiment_tab_id
927
+ if order is None:
928
+ order = last_step.eln_entry.order + 1
929
+ eln_manager = DataMgmtServer.get_eln_manager(protocol.user)
930
+ entry_criteria = ElnEntryCriteria(entry_type, step_name, data_type_name=data_type_name,
931
+ order=order, notebook_experiment_tab_id=tab_id, **kwargs)
932
+ new_entry: ExperimentEntry = eln_manager.add_experiment_entry(protocol.eln_experiment.notebook_experiment_id,
933
+ entry_criteria)
934
+ return eln_manager, new_entry
935
+
882
936
 
883
937
  # TODO: Using this to set the new status field setting.
884
938
  class _GaugeChartDefinition(GaugeChartDefinition):