sapiopycommons 2025.9.16a754__py3-none-any.whl → 2025.9.19a761__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sapiopycommons might be problematic. Click here for more details.

@@ -9,15 +9,18 @@ from sapiopycommons.ai.protoapi.plan.converter.converter_pb2 import ConverterDet
9
9
  ConvertRequestPbo, ConverterDetailsRequestPbo, ContentTypePairPbo
10
10
  from sapiopycommons.ai.protoapi.plan.converter.converter_pb2_grpc import ConverterServiceServicer
11
11
  from sapiopycommons.ai.protoapi.plan.item.item_container_pb2 import ContentTypePbo, StepItemContainerPbo
12
+ from sapiopycommons.files.temp_files import TempFileHandler
12
13
 
13
14
 
14
15
  class ConverterServiceBase(ConverterServiceServicer, ABC):
16
+ debug_mode: bool = False
17
+
15
18
  def GetConverterDetails(self, request: ConverterDetailsRequestPbo, context: ServicerContext) \
16
19
  -> ConverterDetailsResponsePbo:
17
20
  try:
18
21
  supported_types: list[ContentTypePairPbo] = []
19
22
  for c in self.register_converters():
20
- converter = c()
23
+ converter = c(self.debug_mode)
21
24
  supported_types.append(ContentTypePairPbo(
22
25
  converter_name=converter.name(),
23
26
  input_content_type=converter.input_type_pbo(),
@@ -34,12 +37,18 @@ class ConverterServiceBase(ConverterServiceServicer, ABC):
34
37
  input_container: StepItemContainerPbo = request.item_container
35
38
  input_type: ContentTypePbo = input_container.content_type
36
39
  target_type: ContentTypePbo = request.target_content_type
40
+
41
+ use_converter: ConverterBase | None = None
37
42
  for c in self.register_converters():
38
- converter = c()
43
+ converter = c(self.debug_mode)
39
44
  if converter.can_convert(input_type, target_type):
40
- return ConvertResponsePbo(item_container=converter.convert(input_container))
41
- raise ValueError(f"No converter found for converting {input_type.name} ({', '.join(input_type.extensions)}) "
42
- f"to {target_type.name} ({', '.join(target_type.extensions)}).")
45
+ use_converter = converter
46
+ break
47
+ if use_converter is None:
48
+ raise ValueError(f"No converter found for converting {input_type.name} ({', '.join(input_type.extensions)}) "
49
+ f"to {target_type.name} ({', '.join(target_type.extensions)}).")
50
+
51
+ return ConvertResponsePbo(item_container=self.run(use_converter, input_container))
43
52
  except Exception as e:
44
53
  print(f"CRITICAL ERROR: {e}")
45
54
  print(traceback.format_exc())
@@ -62,8 +71,30 @@ class ConverterServiceBase(ConverterServiceServicer, ABC):
62
71
  """
63
72
  pass
64
73
 
74
+ def run(self, converter: ConverterBase, input_container: StepItemContainerPbo) -> StepItemContainerPbo:
75
+ try:
76
+ return converter.convert(input_container)
77
+ finally:
78
+ # Clean up any temporary files created by the converter. If in debug mode, then log the files instead
79
+ # so that they can be manually inspected.
80
+ if self.debug_mode:
81
+ print("Temporary files/directories created during converter execution:")
82
+ for directory in converter.temp_data.directories:
83
+ print(f"\tDirectory: {directory}")
84
+ for file in converter.temp_data.files:
85
+ print(f"\tFile: {file}")
86
+ else:
87
+ converter.temp_data.cleanup()
88
+
65
89
 
66
90
  class ConverterBase(ABC):
91
+ temp_data: TempFileHandler
92
+ debug_mode: bool
93
+
94
+ def __init__(self, debug_mode: bool):
95
+ self.temp_data = TempFileHandler()
96
+ self.debug_mode = debug_mode
97
+
67
98
  def name(self) -> str:
68
99
  """
69
100
  :return: The name of this converter, typically in the format "<input_type> to <output_type>".
@@ -6,11 +6,13 @@ from typing import Any
6
6
 
7
7
  import grpc
8
8
 
9
+ from sapiopycommons.ai.converter_service_base import ConverterServiceBase
9
10
  from sapiopycommons.ai.protoapi.plan.converter.converter_pb2_grpc import add_ConverterServiceServicer_to_server, \
10
11
  ConverterServiceServicer
11
12
  from sapiopycommons.ai.protoapi.plan.script.script_pb2_grpc import add_ScriptServiceServicer_to_server, \
12
13
  ScriptServiceServicer
13
14
  from sapiopycommons.ai.protoapi.plan.tool.tool_pb2_grpc import add_ToolServiceServicer_to_server, ToolServiceServicer
15
+ from sapiopycommons.ai.tool_service_base import ToolServiceBase
14
16
 
15
17
 
16
18
  class SapioGrpcServer:
@@ -78,12 +80,13 @@ class SapioGrpcServer:
78
80
  if option_name in ('grpc.max_send_message_length', 'grpc.max_receive_message_length'):
79
81
  self.options[i] = (option_name, message_mb_size * 1024 * 1024)
80
82
 
81
- def add_converter_service(self, service: ConverterServiceServicer) -> None:
83
+ def add_converter_service(self, service: ConverterServiceBase) -> None:
82
84
  """
83
85
  Add a converter service to the gRPC server.
84
86
 
85
87
  :param service: The converter service to register with the server.
86
88
  """
89
+ service.debug_mode = self.debug_mode
87
90
  self._converter_services.append(service)
88
91
 
89
92
  def add_script_service(self, service: ScriptServiceServicer) -> None:
@@ -94,7 +97,7 @@ class SapioGrpcServer:
94
97
  """
95
98
  self._script_services.append(service)
96
99
 
97
- def add_tool_service(self, service: ToolServiceServicer) -> None:
100
+ def add_tool_service(self, service: ToolServiceBase) -> None:
98
101
  """
99
102
  Add a tool service to the gRPC server.
100
103
 
@@ -9,8 +9,9 @@ import subprocess
9
9
  import traceback
10
10
  from abc import abstractmethod, ABC
11
11
  from logging import Logger
12
+ from os import PathLike
12
13
  from subprocess import CompletedProcess
13
- from typing import Any, Iterable, Mapping
14
+ from typing import Any, Iterable, Mapping, Sequence
14
15
 
15
16
  from grpc import ServicerContext
16
17
  from sapiopylib.rest.User import SapioUser, ensure_logger_initialized
@@ -168,6 +169,9 @@ class JsonResult(SapioToolResult):
168
169
  :param file_extensions: A list of file extensions that this binary data can be saved as.
169
170
  :param name: An optional identifying name for this result that will be accessible to the next tool.
170
171
  """
172
+ # Verify that the given json_data is actually a list of dictionaries.
173
+ if not isinstance(json_data, list) or not all(isinstance(x, dict) for x in json_data):
174
+ raise ValueError("json_data must be a list of dictionaries.")
171
175
  self.json_data = json_data
172
176
  self.content_type = content_type
173
177
  self.file_extensions = file_extensions if file_extensions else ["json"]
@@ -872,7 +876,10 @@ class ToolBase(ABC):
872
876
  """
873
877
  pass
874
878
 
875
- def call_subprocess(self, args: list[str], cwd: str | None = None, **kwargs) -> CompletedProcess[str]:
879
+ def call_subprocess(self,
880
+ args: str | bytes | PathLike[str] | PathLike[bytes] | Sequence[str | bytes | PathLike[str] | PathLike[bytes]],
881
+ cwd: str | bytes | PathLike[str] | PathLike[bytes] | None = None,
882
+ **kwargs) -> CompletedProcess[str]:
876
883
  """
877
884
  Call a subprocess with the given arguments, logging the command and any errors that occur.
878
885
  This function will raise an exception if the return code of the subprocess is non-zero. The output of the
@@ -1002,18 +1009,25 @@ class ToolBase(ABC):
1002
1009
  ret_val.append(row_dict)
1003
1010
  return list(headers), ret_val
1004
1011
 
1005
- def get_input_json(self, index: int = 0) -> list[list[Any]] | list[dict[str, Any]]:
1012
+ def get_input_json(self, index: int = 0) -> list[dict[str, Any]]:
1006
1013
  """
1007
1014
  Parse the JSON data from the request object.
1008
1015
 
1009
1016
  :param index: The index of the input to parse. Defaults to 0. Used for tools that accept multiple inputs.
1010
- :return: A list of parsed JSON objects. Each entry in the list represents a separate JSON entry from the input.
1011
- Depending on this tool, this may be a list of dictionaries or a list of lists.
1017
+ :return: A list of parsed JSON objects, which are represented as dictionaries.
1012
1018
  """
1013
1019
  container: StepItemContainerPbo = self.request.input[index].item_container
1014
1020
  if not container.HasField("json_container"):
1015
1021
  raise Exception(f"Input {index} does not contain a JSON container.")
1016
- return [json.loads(x) for x in container.json_container.items]
1022
+ input_json: list[Any] = [json.loads(x) for x in container.json_container.items]
1023
+ # Verify that the given JSON actually is a list of dictionaries. If they aren't then the previous step provided
1024
+ # bad input. Tools are enforced to result in a list of dictionaries when returning JSON data, so this is likely
1025
+ # an error caused by a script or static input step.
1026
+ for i, entry in enumerate(input_json):
1027
+ if not isinstance(entry, dict):
1028
+ raise Exception(f"Element {i} of input {index} is not a dictionary object. All top-level JSON inputs "
1029
+ f"are expected to be dictionaries.")
1030
+ return input_json
1017
1031
 
1018
1032
  def get_input_text(self, index: int = 0) -> list[str]:
1019
1033
  """
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import shutil
3
3
  import tempfile
4
+ from typing import Callable
4
5
 
5
6
 
6
7
  # FR-47422: Created class.
@@ -17,6 +18,8 @@ class TempFileHandler:
17
18
 
18
19
  def create_temp_directory(self) -> str:
19
20
  """
21
+ Create a temporary directory.
22
+
20
23
  :return: The path to a newly created temporary directory.
21
24
  """
22
25
  directory: str = tempfile.mkdtemp()
@@ -25,6 +28,8 @@ class TempFileHandler:
25
28
 
26
29
  def create_temp_file(self, data: str | bytes, suffix: str = "") -> str:
27
30
  """
31
+ Create a temporary file with the specified data and optional suffix.
32
+
28
33
  :param data: The data to write to the temporary file.
29
34
  :param suffix: An optional suffix for the temporary file.
30
35
  :return: The path to a newly created temporary file containing the provided data.
@@ -36,6 +41,29 @@ class TempFileHandler:
36
41
  self.files.append(file_path)
37
42
  return file_path
38
43
 
44
+ def create_temp_file_from_func(self, func: Callable, is_binary: bool = True, suffix: str = "", **kwargs) -> str:
45
+ """
46
+ Create a temporary file and populate it using the provided function. The function should accept parameters as
47
+ specified in the `params` dictionary. Any parameter in `params` with the value "<NEW_FILE>" will be replaced
48
+ with the path of the created temporary file.
49
+
50
+ :param func: The function to call with the temporary file path that will populate the file.
51
+ :param is_binary: Whether to open the temporary file in binary mode.
52
+ :param suffix: An optional suffix for the temporary file.
53
+ :param kwargs: Keyword arguments to pass to the function. Use "<NEW_FILE>" as a value to indicate where the
54
+ temporary file path should be inserted.
55
+ :return: The path to the newly created temporary file.
56
+ """
57
+ mode: str = 'wb' if is_binary else 'w'
58
+ with tempfile.NamedTemporaryFile(mode, suffix=suffix, delete=False) as tmp_file:
59
+ file_path: str = tmp_file.name
60
+ for key, value in kwargs.items():
61
+ if value == "<NEW_FILE>":
62
+ kwargs[key] = file_path
63
+ func(**kwargs)
64
+ self.files.append(file_path)
65
+ return file_path
66
+
39
67
  def cleanup(self) -> None:
40
68
  """
41
69
  Delete all temporary files and directories created by this handler.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2025.9.16a754
3
+ Version: 2025.9.19a761
4
4
  Summary: Official Sapio Python API Utilities Package
5
5
  Project-URL: Homepage, https://github.com/sapiosciences
6
6
  Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
@@ -1,11 +1,11 @@
1
1
  sapiopycommons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  sapiopycommons/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- sapiopycommons/ai/converter_service_base.py,sha256=TMSyEekbbqMk9dRuAtLlSJ1sA1H8KpyCDlSOeqGFMWI,5115
3
+ sapiopycommons/ai/converter_service_base.py,sha256=HiUXmwqv1STgyQeF9_eTFXzjIFXp5-NJ7sEhMpV3aAU,6351
4
4
  sapiopycommons/ai/protobuf_utils.py,sha256=cBjbxoFAwU02kNUxEce95WnMU2CMuDD-qFaeWgvQJMQ,24599
5
- sapiopycommons/ai/server.py,sha256=XDm_mj1yWHw-xQRFsFRHnsGw2JU0wsW2mR22P8PB09A,5744
5
+ sapiopycommons/ai/server.py,sha256=GtkSKeHhahd40DXU6XIY3ZKpMl-MPMgPUYL8TakTj0w,5919
6
6
  sapiopycommons/ai/test_client.py,sha256=Um93jXmIx0YaHf-YbV5NSamPTHveJ0kU_UaAfOApnZg,16342
7
7
  sapiopycommons/ai/tool_of_tools.py,sha256=zYmQ4rNX-qYQnc-vNDnYZjtv9JgmQAmVVuHfVOdBF3w,46984
8
- sapiopycommons/ai/tool_service_base.py,sha256=x19ttJpLEyM-X-O0n0FG_BJktp0Vc6FL_2gKztV8nww,52892
8
+ sapiopycommons/ai/tool_service_base.py,sha256=OmGUDsDj7sCNtjhJVohc1mUQ0PZsMhhgfG8BXAWujGs,53861
9
9
  sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.py,sha256=8tKXwLXcqFGdQHHSEBSi6Fd7dcaCFoOqmhjzqhenb_M,2372
10
10
  sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.pyi,sha256=FwtXmNAf7iYGEFm4kbqb04v77jNHbZg18ZmEDhle_bU,1444
11
11
  sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2_grpc.py,sha256=uO25bcnfGqXpP4ggUur54Nr73Wj-DGWftExzLNcxdHI,931
@@ -69,7 +69,7 @@ sapiopycommons/files/file_text_converter.py,sha256=Gaj_divTiKXWd6flDOgrxNXpcn9fD
69
69
  sapiopycommons/files/file_util.py,sha256=djouyGjsYgWzjz2OBRnSeMDgj6NrsJUm1a2J93J8Wco,31915
70
70
  sapiopycommons/files/file_validator.py,sha256=ryg22-93csmRO_Pv0ZpWphNkB74xWZnHyJ23K56qLj0,28761
71
71
  sapiopycommons/files/file_writer.py,sha256=hACVl0duCjP28gJ1NPljkjagNCLod0ygUlPbvUmRDNM,17605
72
- sapiopycommons/files/temp_files.py,sha256=sw9Uw1ebhKzKcjE0VV7EcIA0UlySypGf90LlnJxYDiY,1602
72
+ sapiopycommons/files/temp_files.py,sha256=e1PV07wFN5At1Ob-TbfS9nsPiZ8u9_Fl22KnWxJMmac,3056
73
73
  sapiopycommons/flowcyto/flow_cyto.py,sha256=B6DFquLi-gcWfJWyP4vYfwTXXJKl6O9W5-k8FzkM0Oo,2610
74
74
  sapiopycommons/flowcyto/flowcyto_data.py,sha256=mYKFuLbtpJ-EsQxLGtu4tNHVlygTxKixgJxJqD68F58,2596
75
75
  sapiopycommons/general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -102,7 +102,7 @@ sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
102
102
  sapiopycommons/webhook/webhook_context.py,sha256=D793uLsb1691SalaPnBUk3rOSxn_hYLhdvkaIxjNXss,1909
103
103
  sapiopycommons/webhook/webhook_handlers.py,sha256=7o_wXOruhT9auNh8OfhJAh4WhhiPKij67FMBSpGPICc,39939
104
104
  sapiopycommons/webhook/webservice_handlers.py,sha256=cvW6Mk_110BzYqkbk63Kg7jWrltBCDALOlkJRu8h4VQ,14300
105
- sapiopycommons-2025.9.16a754.dist-info/METADATA,sha256=2ypbpQ44RejG2I4DXVHTd4UDrmzOWdbDCjArNSGAu_Y,3142
106
- sapiopycommons-2025.9.16a754.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
107
- sapiopycommons-2025.9.16a754.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
108
- sapiopycommons-2025.9.16a754.dist-info/RECORD,,
105
+ sapiopycommons-2025.9.19a761.dist-info/METADATA,sha256=As9JoZ85SMYRqYv83_ri1EKVYvMbU3dNWvkIVZ5RGA4,3142
106
+ sapiopycommons-2025.9.19a761.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
107
+ sapiopycommons-2025.9.19a761.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
108
+ sapiopycommons-2025.9.19a761.dist-info/RECORD,,