sapiopycommons 2025.10.1a770__tar.gz → 2025.10.2a773__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 (126) hide show
  1. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/PKG-INFO +1 -1
  2. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/pyproject.toml +1 -1
  3. sapiopycommons-2025.10.2a773/src/sapiopycommons/ai/request_validation.py +451 -0
  4. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/tool_service_base.py +7 -7
  5. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/.gitignore +0 -0
  6. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/LICENSE +0 -0
  7. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/README.md +0 -0
  8. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/__init__.py +0 -0
  9. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/__init__.py +0 -0
  10. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/converter_service_base.py +0 -0
  11. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.py +0 -0
  12. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.pyi +0 -0
  13. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2_grpc.py +0 -0
  14. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.py +0 -0
  15. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.pyi +0 -0
  16. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2_grpc.py +0 -0
  17. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/converter/converter_pb2.py +0 -0
  18. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/converter/converter_pb2.pyi +0 -0
  19. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/converter/converter_pb2_grpc.py +0 -0
  20. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/item/item_container_pb2.py +0 -0
  21. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/item/item_container_pb2.pyi +0 -0
  22. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/item/item_container_pb2_grpc.py +0 -0
  23. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/script/script_pb2.py +0 -0
  24. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/script/script_pb2.pyi +0 -0
  25. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/script/script_pb2_grpc.py +0 -0
  26. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/step_output_pb2.py +0 -0
  27. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/step_output_pb2.pyi +0 -0
  28. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/step_output_pb2_grpc.py +0 -0
  29. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/step_pb2.py +0 -0
  30. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/step_pb2.pyi +0 -0
  31. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/step_pb2_grpc.py +0 -0
  32. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/tool/entry_pb2.py +0 -0
  33. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/tool/entry_pb2.pyi +0 -0
  34. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/tool/entry_pb2_grpc.py +0 -0
  35. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/tool/tool_pb2.py +0 -0
  36. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/tool/tool_pb2.pyi +0 -0
  37. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/plan/tool/tool_pb2_grpc.py +0 -0
  38. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.py +0 -0
  39. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.pyi +0 -0
  40. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2_grpc.py +0 -0
  41. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/protobuf_utils.py +0 -0
  42. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/server.py +0 -0
  43. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/ai/test_client.py +0 -0
  44. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/callbacks/__init__.py +0 -0
  45. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/callbacks/callback_util.py +0 -0
  46. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/callbacks/field_builder.py +0 -0
  47. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/chem/IndigoMolecules.py +0 -0
  48. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/chem/Molecules.py +0 -0
  49. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/chem/__init__.py +0 -0
  50. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/customreport/__init__.py +0 -0
  51. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/customreport/auto_pagers.py +0 -0
  52. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/customreport/column_builder.py +0 -0
  53. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/customreport/custom_report_builder.py +0 -0
  54. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/customreport/term_builder.py +0 -0
  55. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/datatype/__init__.py +0 -0
  56. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/datatype/attachment_util.py +0 -0
  57. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/datatype/data_fields.py +0 -0
  58. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/datatype/pseudo_data_types.py +0 -0
  59. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/eln/__init__.py +0 -0
  60. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/eln/experiment_cache.py +0 -0
  61. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/eln/experiment_handler.py +0 -0
  62. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/eln/experiment_report_util.py +0 -0
  63. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/eln/experiment_step_factory.py +0 -0
  64. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/eln/experiment_tags.py +0 -0
  65. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/eln/plate_designer.py +0 -0
  66. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/eln/step_creation.py +0 -0
  67. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/files/__init__.py +0 -0
  68. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/files/assay_plate_reader.py +0 -0
  69. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/files/complex_data_loader.py +0 -0
  70. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/files/file_bridge.py +0 -0
  71. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/files/file_bridge_handler.py +0 -0
  72. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/files/file_data_handler.py +0 -0
  73. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/files/file_text_converter.py +0 -0
  74. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/files/file_util.py +0 -0
  75. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/files/file_validator.py +0 -0
  76. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/files/file_writer.py +0 -0
  77. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/files/temp_files.py +0 -0
  78. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/flowcyto/flow_cyto.py +0 -0
  79. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/flowcyto/flowcyto_data.py +0 -0
  80. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/general/__init__.py +0 -0
  81. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/general/accession_service.py +0 -0
  82. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/general/aliases.py +0 -0
  83. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/general/audit_log.py +0 -0
  84. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/general/custom_report_util.py +0 -0
  85. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/general/data_structure_util.py +0 -0
  86. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/general/directive_util.py +0 -0
  87. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/general/exceptions.py +0 -0
  88. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/general/html_formatter.py +0 -0
  89. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/general/popup_util.py +0 -0
  90. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/general/sapio_links.py +0 -0
  91. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/general/storage_util.py +0 -0
  92. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/general/time_util.py +0 -0
  93. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/multimodal/multimodal.py +0 -0
  94. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/multimodal/multimodal_data.py +0 -0
  95. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/processtracking/__init__.py +0 -0
  96. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/processtracking/custom_workflow_handler.py +0 -0
  97. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/processtracking/endpoints.py +0 -0
  98. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/recordmodel/__init__.py +0 -0
  99. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/recordmodel/record_handler.py +0 -0
  100. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/rules/__init__.py +0 -0
  101. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/rules/eln_rule_handler.py +0 -0
  102. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/rules/on_save_rule_handler.py +0 -0
  103. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/samples/aliquot.py +0 -0
  104. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/sftpconnect/__init__.py +0 -0
  105. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/sftpconnect/sftp_builder.py +0 -0
  106. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/webhook/__init__.py +0 -0
  107. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/webhook/webhook_context.py +0 -0
  108. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/webhook/webhook_handlers.py +0 -0
  109. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/src/sapiopycommons/webhook/webservice_handlers.py +0 -0
  110. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/AF-A0A009IHW8-F1-model_v4.cif +0 -0
  111. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/_do_not_add_init_py_here +0 -0
  112. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/accession_test.py +0 -0
  113. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/aliquot_test.py +0 -0
  114. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/assay_plate_reader/BMGLabtech96.txt +0 -0
  115. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/assay_plate_reader/assay_plate_processing_test.py +0 -0
  116. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/bio_reg_test.py +0 -0
  117. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/chem_test.py +0 -0
  118. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/chem_test_curation_queue.py +0 -0
  119. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/curation_queue_test.sdf +0 -0
  120. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/data_type_models.py +0 -0
  121. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/file_compression_tests.py +0 -0
  122. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/flowcyto/8_color_ICS.wsp +0 -0
  123. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/flowcyto_test.py +0 -0
  124. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/kappa.chains.fasta +0 -0
  125. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/mafft_test.py +0 -0
  126. {sapiopycommons-2025.10.1a770 → sapiopycommons-2025.10.2a773}/tests/test.gb +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2025.10.1a770
3
+ Version: 2025.10.2a773
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.10.01a770'
7
+ version='2025.10.02a773'
8
8
  authors = [
9
9
  { name="Jonathan Steck", email="jsteck@sapiosciences.com" },
10
10
  { name="Yechen Qiao", email="yqiao@sapiosciences.com" },
@@ -0,0 +1,451 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC
4
+ from typing import Any, Callable
5
+
6
+ from sapiopycommons.ai.test_client import ContainerType
7
+ from sapiopycommons.ai.tool_service_base import ToolBase
8
+
9
+
10
+ class InputValidation(ABC):
11
+ """
12
+ A base class for validating the input to a tool.
13
+ """
14
+ index: int
15
+ max_entries: int | None
16
+ allow_empty_input: bool
17
+ allow_empty_entries: bool
18
+
19
+ def __init__(self, index: int, max_entries: int | None = None,
20
+ allow_empty_input: bool = False, allow_empty_entries: bool = False):
21
+ """
22
+ :param index: The index of the input to validate.
23
+ :param max_entries: The maximum number of entries allowed for this input. If None, then there is no limit.
24
+ :param allow_empty_input: If true, then the input can be completely empty.
25
+ :param allow_empty_entries: If true, then individual entries in the input can be empty.
26
+ """
27
+ self.index = index
28
+ self.max_entries = max_entries
29
+ self.allow_empty_input = allow_empty_input
30
+ self.allow_empty_entries = allow_empty_entries
31
+
32
+
33
+ class BinaryValidation(InputValidation):
34
+ """
35
+ A class representing a validation requirement for a binary input.
36
+ """
37
+ func: Callable[[int, bytes], list[str]] | None
38
+
39
+ def __init__(self, index: int, max_entries: int | None = None,
40
+ allow_empty_input: bool = False, allow_empty_entries: bool = False,
41
+ func: Callable[[int, bytes], list[str]] | None = None):
42
+ """
43
+ :param index: The index of the input to validate.
44
+ :param max_entries: The maximum number of entries allowed for this input. If None, then there is no limit.
45
+ :param allow_empty_input: If true, then the input can be completely empty.
46
+ :param allow_empty_entries: If true, then individual entries in the input can be empty
47
+ :param func: An optional function to run on each entry in the input. The function should take the index of
48
+ the input and the entry as arguments, and return a list of error messages if the entry is not valid. If the
49
+ entry is valid, the function should return an empty list. This function will not be called if the input or
50
+ entry are empty.
51
+ """
52
+ super().__init__(index, max_entries, allow_empty_input, allow_empty_entries)
53
+ self.func = func
54
+
55
+
56
+ class CsvValidation(InputValidation):
57
+ """
58
+ A class representing a validation requirement for a CSV input.
59
+ """
60
+ required_headers: list[str] | None = None
61
+
62
+ func: Callable[[int, dict[str, Any]], list[str]] | None
63
+
64
+ def __init__(self, index: int, max_entries: int | None = None,
65
+ allow_empty_input: bool = False, allow_empty_entries: bool = False,
66
+ required_headers: list[str] | None = None,
67
+ func: Callable[[int, dict[str, Any]], list[str]] | None = None):
68
+ """
69
+ :param index: The index of the input to validate.
70
+ :param max_entries: The maximum number of entries allowed for this input. If None, then there is no limit.
71
+ :param allow_empty_input: If true, then the input can be completely empty.
72
+ :param allow_empty_entries: If true, then individual entries in the input can be empty.
73
+ :param required_headers: A list of headers that must be present in the CSV input. If None, then no header
74
+ validation will be performed.
75
+ :param func: An optional function to run on each entry in the input. The function should take the index of
76
+ the input and the entry as arguments, and return a list of error messages if the entry is not valid. If the
77
+ entry is valid, the function should return an empty list. This function will not be called if the input or
78
+ entry are empty.
79
+ """
80
+ super().__init__(index, max_entries, allow_empty_input, allow_empty_entries)
81
+ self.required_headers = required_headers
82
+ self.func = func
83
+
84
+
85
+ class JsonValidation(InputValidation):
86
+ """
87
+ A class representing a validation requirement for a JSON input.
88
+ """
89
+ json_requirements: dict[str, JsonKeyValidation]
90
+
91
+ func: Callable[[int, dict[str, Any]], list[str]] | None
92
+
93
+ def __init__(self, index: int, max_entries: int | None = None,
94
+ allow_empty_input: bool = False, allow_empty_entries: bool = False,
95
+ json_requirements: list[JsonKeyValidation] | None = None,
96
+ func: Callable[[int, dict[str, Any]], list[str]] | None = None):
97
+ """
98
+ :param index: The index of the input to validate.
99
+ :param max_entries: The maximum number of entries allowed for this input. If None, then there is no limit.
100
+ :param allow_empty_input: If true, then the input can be completely empty.
101
+ :param allow_empty_entries: If true, then individual entries in the input can be empty.
102
+ :param json_requirements: A list of JSON requirements to validate for JSON inputs. Each requirement
103
+ specifies a key to validate, the expected type of the value for that key, and any nested requirements
104
+ for that key. Only applicable to JSON inputs.
105
+ :param func: An optional function to run on each entry in the input. The function should take the index of
106
+ the input and the entry as arguments, and return a list of error messages if the entry is not valid. If the
107
+ entry is valid, the function should return an empty list. This function will not be called if the input or
108
+ entry are empty.
109
+ """
110
+ super().__init__(index, max_entries, allow_empty_input, allow_empty_entries)
111
+ self.json_requirements = {}
112
+ if json_requirements:
113
+ for req in json_requirements:
114
+ if req.key in self.json_requirements:
115
+ raise ValueError(f"Duplicate JSON requirement key {req.key} for input index {index}.")
116
+ self.json_requirements[req.key] = req
117
+
118
+ self.func = func
119
+
120
+
121
+ class JsonKeyValidation:
122
+ """
123
+ A class representing a validation requirement for a specific key in a JSON input.
124
+ """
125
+ key: str
126
+ json_type: type
127
+ required: bool
128
+ allow_empty: bool
129
+
130
+ list_type: type | None = None
131
+ nested_requirements: dict[str, JsonKeyValidation]
132
+
133
+ func: Callable[[str, Any], list[str]] | None = None
134
+
135
+ def __init__(self, key: str, json_type: type, required: bool = True, allow_empty: bool = False,
136
+ list_type: type | None = None, nested_requirements: list[JsonKeyValidation] | None = None,
137
+ func: Callable[[str, Any], list[str]] | None = None):
138
+ """
139
+ :param key: The key in the JSON input to validate.
140
+ :param json_type: The expected type of the value for this key. This should be one of: str, int, float, bool,
141
+ list, or dict.
142
+ :param required: If true, then this key must be present in the JSON input. If false, then the key is optional,
143
+ but if present, it must still match the other expected criteria.
144
+ :param allow_empty: If true, then the value for this key can be empty (e.g., an empty string, list, or dict).
145
+ If false, then the value must not be empty.
146
+ :param list_type: The expected type of the entries in the list if json_type is list.
147
+ :param nested_requirements: A list of nested JSON requirements to validate for this key if it is a dict. Each
148
+ requirement specifies a key to validate, the expected type of the value for that key, and any nested
149
+ requirements for that key. Only applicable if json_type is dict, or if json_type is list and list_type is
150
+ dict.
151
+ :param func: An optional function to run on the value for this key. The function should take the path and the
152
+ value as arguments, and return a list of error messages if the value is not valid. If the value is valid,
153
+ the function should return an empty list. This function will not be called if the key is missing,
154
+ the value is of the wrong type, or the value is an empty str/list/dict and allow_empty is false.
155
+ """
156
+ self.key = key
157
+ self.json_type = json_type
158
+ self.required = required
159
+
160
+ self.list_type = list_type
161
+ self.nested_requirements = {}
162
+ if nested_requirements:
163
+ for req in nested_requirements:
164
+ if req.key in self.nested_requirements:
165
+ raise ValueError(f"Duplicate nested requirement key {req.key} for JSON key {key}.")
166
+ self.nested_requirements[req.key] = req
167
+
168
+ self.func = func
169
+
170
+ allowed_types: set[type] = {str, int, float, bool, list, dict}
171
+ if self.json_type not in allowed_types:
172
+ raise ValueError(f"Invalid json_type {self.json_type} for key {key}. Must be one of: "
173
+ f"{', '.join([t.__name__ for t in allowed_types])}.")
174
+ if self.list_type is not None and self.list_type not in allowed_types:
175
+ raise ValueError(f"Invalid list_type {self.list_type} for key {key}. Must be one of: "
176
+ f"{', '.join([t.__name__ for t in allowed_types])}.")
177
+
178
+
179
+ class TextValidation(InputValidation):
180
+ """
181
+ A class representing a validation requirement for a text input.
182
+ """
183
+ flatten: bool
184
+ regex: str | None = None
185
+
186
+ func: Callable[[int, str], list[str]] | None = None
187
+
188
+ def __init__(self, index: int, max_entries: int | None = None,
189
+ allow_empty_input: bool = False, allow_empty_entries: bool = False, flatten: bool = False,
190
+ regex: str | None = None, func: Callable[[int, str], list[str]] | None = None):
191
+ """
192
+ :param index: The index of the input to validate.
193
+ :param max_entries: The maximum number of entries allowed for this input. If None, then there is no limit.
194
+ :param allow_empty_input: If true, then the input can be completely empty.
195
+ :param allow_empty_entries: If true, then individual entries in the input can be empty.
196
+ :param flatten: If true, then the input will be flattened before validation
197
+ :param regex: An optional regular expression that each entry in the input must fully match. If None, then no
198
+ regex validation will be performed. This function will not be called if the input or entry are empty.
199
+ :param func: An optional function to run on each entry in the input. The function should take the index of
200
+ the input and the entry as arguments, and return a list of error messages if the entry is not valid. If the
201
+ entry is valid, the function should return an empty list. This function will not be called if the input or
202
+ entry are empty. If a regex is provided, the function will only be called if the entry matches the regex.
203
+ """
204
+ super().__init__(index, max_entries, allow_empty_input, allow_empty_entries)
205
+ self.flatten = flatten
206
+ self.regex = regex
207
+ self.func = func
208
+
209
+
210
+
211
+ class InputValidator:
212
+ """
213
+ A class for validating the inputs to a tool based on their container types and specified validation requirements.
214
+ """
215
+ tool: ToolBase
216
+ requirements: dict[int, InputValidation]
217
+
218
+ def __init__(self, tool: ToolBase, requirements: list[InputValidation] | None = None):
219
+ """
220
+ :param tool: The tool to validate the request of.
221
+ :param requirements: A list of validation requirements to apply to the request. If a validation object is
222
+ not provided for a given input, then default validation will be applied. Default validation requires that
223
+ the input is not empty, and that the entries in the input are not empty.
224
+ """
225
+ self.tool = tool
226
+ self.requirements = {}
227
+ for req in requirements:
228
+ if req.index < 0 or req.index >= len(tool.input_configs):
229
+ raise ValueError(f"Validation requirement index {req.index} is out of range for tool "
230
+ f"{tool.name()} with {len(tool.input_configs)} inputs.")
231
+ if req.index in self.requirements:
232
+ raise ValueError(f"Duplicate validation requirement index {req.index} for tool {tool.name()}.")
233
+ self.requirements[req.index] = req
234
+
235
+ def run(self) -> list[str]:
236
+ """
237
+ Run simple validation on all the inputs based on their container types. This requires the following:
238
+ - The input may not be empty.
239
+ - The entries in the input may not be empty, unless allow_empty is set to true.
240
+ - If provided, the number of entries in the input may not exceed a maximum size.
241
+ - If provided, certain keys must be present in the JSON input, and they must match the above behavior.
242
+
243
+ :return: A list of the error messages if the request is not valid. If the request is valid, return an empty
244
+ list.
245
+ """
246
+ errors: list[str] = []
247
+ for i, input_type in enumerate(self.tool.input_container_types):
248
+ match input_type:
249
+ case ContainerType.BINARY:
250
+ r: InputValidation = self.requirements.get(i, BinaryValidation(i))
251
+ if not isinstance(r, BinaryValidation):
252
+ raise ValueError(f"Validation requirement for binary input at index {i} must be a "
253
+ f"BinaryValidation object. Got {type(r)} instead.")
254
+ errors.extend(self.validate_input_binary(i, r))
255
+ case ContainerType.CSV:
256
+ r: InputValidation = self.requirements.get(i, CsvValidation(i))
257
+ if not isinstance(r, CsvValidation):
258
+ raise ValueError(f"Validation requirement for CSV input at index {i} must be a "
259
+ f"CsvValidation object. Got {type(r)} instead.")
260
+ errors.extend(self.validate_input_csv(i, r))
261
+ case ContainerType.JSON:
262
+ r: InputValidation = self.requirements.get(i, JsonValidation(i))
263
+ if not isinstance(r, JsonValidation):
264
+ raise ValueError(f"Validation requirement for JSON input at index {i} must be a "
265
+ f"JsonValidation object. Got {type(r)} instead.")
266
+ errors.extend(self.validate_input_json(i, r))
267
+ case ContainerType.TEXT:
268
+ r: InputValidation = self.requirements.get(i, TextValidation(i))
269
+ if not isinstance(r, TextValidation):
270
+ raise ValueError(f"Validation requirement for text input at index {i} must be a "
271
+ f"TextValidation object. Got {type(r)} instead.")
272
+ errors.extend(self.validate_input_text(i, r))
273
+ return errors
274
+
275
+ def validate_input_binary(self, index: int, r: BinaryValidation) -> list[str]:
276
+ """
277
+ Run simple validation on the binary input at the given index.
278
+
279
+ :param index: The index of the input to validate.
280
+ :param r: The validation requirement to use for this input.
281
+ :return: A list of error messages if the input is not valid. If the input is valid, return an empty list.
282
+ """
283
+ input_files: list[bytes] = self.tool.get_input_binary(index)
284
+ errors: list[str] = []
285
+ if not input_files:
286
+ if not r.allow_empty_input:
287
+ errors.append(f"Input {index} is empty.")
288
+ elif r.max_entries is not None and len(input_files) > r.max_entries:
289
+ errors.append(f"Input {index} contains {len(input_files)} entries, which exceeds the maximum allowed "
290
+ f"number of {r.max_entries}.")
291
+ elif not r.allow_empty_entries or r.func:
292
+ for i, entry in enumerate(input_files):
293
+ if not entry.strip():
294
+ if not r.allow_empty_entries:
295
+ errors.append(f"Entry {i} of input {index} is empty or contains only whitespace.")
296
+ elif r.func:
297
+ errors.extend(r.func(i, entry))
298
+ return errors
299
+
300
+ def validate_input_csv(self, index: int, r: CsvValidation) -> list[str]:
301
+ """
302
+ Run simple validation on the CSV input at the given index.
303
+
304
+ :param index: The index of the input to validate.
305
+ :param r: The validation requirement to use for this input.
306
+ :return: A list of error messages if the input is not valid. If the input is valid, return an empty list.
307
+ """
308
+ headers, csv = self.tool.get_input_csv(index)
309
+ headers: list[str]
310
+ csv: list[dict[str, Any]]
311
+
312
+ errors: list[str] = []
313
+ if r.required_headers:
314
+ missing_headers: list[str] = [h for h in r.required_headers if h not in headers]
315
+ if missing_headers:
316
+ errors.append(f"Input {index} is missing required headers: {', '.join(missing_headers)}.")
317
+
318
+ if not csv:
319
+ if not r.allow_empty_input:
320
+ errors.append(f"Input {index} is empty.")
321
+ elif r.max_entries is not None and len(csv) > r.max_entries:
322
+ errors.append(f"Input {index} contains {len(csv)} entries, which exceeds the maximum allowed "
323
+ f"number of {r.max_entries}.")
324
+ elif not r.allow_empty_entries or r.func:
325
+ for i, entry in enumerate(csv):
326
+ if not entry or all(not cell.strip() for cell in entry):
327
+ if not r.allow_empty_entries:
328
+ errors.append(f"Entry {i} of input {index} is empty or contains only whitespace.")
329
+ elif r.func:
330
+ errors.extend(r.func(i, entry))
331
+ return errors
332
+
333
+ def validate_input_json(self, index: int, r: JsonValidation) -> list[str]:
334
+ """
335
+ Run simple validation on the JSON input at the given index.
336
+
337
+ :param index: The index of the input to validate.
338
+ :param r: The validation requirement to use for this input.
339
+ :return: A list of error messages if the input is not valid. If the input is valid, return an empty list.
340
+ """
341
+ input_json: list[dict[str, Any]] = self.tool.get_input_json(index)
342
+ errors: list[str] = []
343
+ if not input_json:
344
+ if not r.allow_empty_input:
345
+ errors.append(f"Input {index} is empty.")
346
+ elif r.max_entries is not None and len(input_json) > r.max_entries:
347
+ errors.append(f"Input {index} contains {len(input_json)} entries, which exceeds the maximum allowed "
348
+ f"number of {r.max_entries}.")
349
+ elif not r.allow_empty_entries or r.func:
350
+ for i, entry in enumerate(input_json):
351
+ if not entry:
352
+ if not r.allow_empty_entries:
353
+ errors.append(f"Entry {i} of input {index} is empty.")
354
+ elif r.func:
355
+ errors.extend(r.func(i, entry))
356
+
357
+ for key, rk in r.json_requirements.items():
358
+ for i, entry in enumerate(input_json):
359
+ errors.extend(self._validate_input_json_key(entry, rk, f"input[{index}][{i}]"))
360
+
361
+ return errors
362
+
363
+ def _validate_input_json_key(self, data: dict[str, Any], rk: JsonKeyValidation, path: str) -> list[str]:
364
+ """
365
+ Recursively validate a JSON key in a JSON object.
366
+
367
+ :param data: The JSON object to validate.
368
+ :param rk: The JSON key validation requirement to use.
369
+ :param path: The path to the current JSON object, for error reporting.
370
+ :return: A list of error messages if the JSON object is not valid. If the JSON object is valid, return an empty
371
+ list.
372
+ """
373
+ errors: list[str] = []
374
+ if rk.key not in data:
375
+ if rk.required:
376
+ errors.append(f"Missing required key '{rk.key}' at path '{path}'.")
377
+ return errors
378
+
379
+ value: Any = data[rk.key]
380
+ if not isinstance(value, rk.json_type):
381
+ errors.append(f"Key '{rk.key}' at path '{path}' is expected to be of type "
382
+ f"{rk.json_type.__name__}, but got {type(value).__name__}.")
383
+ return errors
384
+
385
+ if isinstance(value, (str, list, dict)) and not value:
386
+ if not rk.allow_empty:
387
+ errors.append(f"Key '{rk.key}' at path '{path}' is empty, but empty values are not allowed.")
388
+ return errors
389
+
390
+ correct_type: bool = True
391
+ if rk.json_type is list and rk.list_type is not None:
392
+ if not isinstance(value, list):
393
+ raise RuntimeError("This should never happen; value was already checked to be of type list.")
394
+ for i, item in enumerate(value):
395
+ if not isinstance(item, rk.list_type):
396
+ errors.append(f"Entry {i} of list key '{rk.key}' at path '{path}' is expected to be of type "
397
+ f"{rk.list_type.__name__}, but got {type(item).__name__}.")
398
+ correct_type = False
399
+ elif rk.list_type is dict and rk.nested_requirements:
400
+ if not isinstance(item, dict):
401
+ raise RuntimeError("This should never happen; item was already checked to be of type dict.")
402
+ for nk, nrk in rk.nested_requirements.items():
403
+ errors.extend(self._validate_input_json_key(item, nrk, f"{path}.{rk.key}[{i}]"))
404
+
405
+ elif rk.json_type is dict and rk.nested_requirements:
406
+ if not isinstance(value, dict):
407
+ raise RuntimeError("This should never happen; value was already checked to be of type dict.")
408
+ for nk, nrk in rk.nested_requirements.items():
409
+ errors.extend(self._validate_input_json_key(value, nrk, f"{path}.{rk.key}"))
410
+
411
+ if rk.func and correct_type:
412
+ errors.extend(rk.func(f"{path}.{rk.key}", value))
413
+
414
+ return errors
415
+
416
+ def validate_input_text(self, index: int, r: TextValidation) -> list[str]:
417
+ """
418
+ Run simple validation on the binary input at the given index.
419
+
420
+ :param index: The index of the input to validate.
421
+ :param r: The validation requirement to use for this input.
422
+ :return: A list of error messages if the input is not valid. If the input is valid, return an empty list.
423
+ """
424
+ input_text: list[str] = self.tool.get_input_text(index)
425
+ if r.flatten:
426
+ input_text = self.tool.flatten_text(input_text)
427
+
428
+ errors: list[str] = []
429
+ if not input_text:
430
+ if not r.allow_empty_input:
431
+ errors.append(f"Input {index} is empty.")
432
+ elif r.max_entries is not None and len(input_text) > r.max_entries:
433
+ errors.append(f"Input {index} contains {len(input_text)} entries, which exceeds the maximum allowed "
434
+ f"number of {r.max_entries}.")
435
+ elif not r.allow_empty_entries or r.regex or r.func:
436
+ for i, entry in enumerate(input_text):
437
+ if not entry.strip():
438
+ if not r.allow_empty_entries:
439
+ errors.append(f"Entry {i} of input {index} is empty or contains only whitespace.")
440
+ elif r.regex:
441
+ import re
442
+ if not re.fullmatch(r.regex, entry):
443
+ errors.append(f"Entry {i} of input {index} does not fully match the expected regex format "
444
+ f"{r.regex}.")
445
+ elif r.func:
446
+ errors.extend(r.func(i, entry))
447
+ if errors and r.flatten:
448
+ errors.append(f"Note that input flattening is enabled for input {index}, which may increase the number "
449
+ f"of entries reported in the above errors. Flattening splits each entry on newlines, removes "
450
+ f"empty lines, and iterates over every line in the input as opposed to each entry as a whole.")
451
+ return errors
@@ -378,9 +378,9 @@ class ToolBase(ABC):
378
378
  A base class for implementing a tool.
379
379
  """
380
380
  input_configs: list[ToolInputDetailsPbo]
381
- _input_container_types: list[ContainerType]
381
+ input_container_types: list[ContainerType]
382
382
  output_configs: list[ToolOutputDetailsPbo]
383
- _output_container_types: list[ContainerType]
383
+ output_container_types: list[ContainerType]
384
384
  config_fields: list[VeloxFieldDefPbo]
385
385
 
386
386
  logs: list[str]
@@ -454,9 +454,9 @@ class ToolBase(ABC):
454
454
 
455
455
  def __init__(self):
456
456
  self.input_configs = []
457
- self._input_container_types = []
457
+ self.input_container_types = []
458
458
  self.output_configs = []
459
- self._output_container_types = []
459
+ self.output_container_types = []
460
460
  self.config_fields = []
461
461
  self.temp_data = TempFileHandler()
462
462
  self.logs = []
@@ -532,7 +532,7 @@ class ToolBase(ABC):
532
532
  max_page_size=page_size[1] if page_size else None,
533
533
  max_request_bytes=max_request_bytes,
534
534
  ))
535
- self._input_container_types.append(container_type)
535
+ self.input_container_types.append(container_type)
536
536
 
537
537
  def add_output(self, container_type: ContainerType, content_type: str, display_name: str, description: str,
538
538
  testing_example: str | bytes, structure_example: str | bytes | None = None) -> None:
@@ -579,7 +579,7 @@ class ToolBase(ABC):
579
579
  structure_example=structure,
580
580
  testing_example=testing
581
581
  )))
582
- self._output_container_types.append(container_type)
582
+ self.output_container_types.append(container_type)
583
583
 
584
584
  def add_config_field(self, field: VeloxFieldDefPbo) -> None:
585
585
  """
@@ -828,7 +828,7 @@ class ToolBase(ABC):
828
828
  corresponds to a separate output from the tool.
829
829
  """
830
830
  results: list[SapioToolResult] = []
831
- for output, container_type in zip(self.output_configs, self._output_container_types):
831
+ for output, container_type in zip(self.output_configs, self.output_container_types):
832
832
  config: ToolIoConfigBasePbo = output.base_config
833
833
  example: ExampleContainerPbo = config.testing_example
834
834
  content_type: str = config.content_type