sapiopycommons 2025.8.15a705__py3-none-any.whl → 2025.8.15a706__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.

Files changed (46) hide show
  1. sapiopycommons/ai/tool_of_tools.py +917 -0
  2. sapiopycommons/callbacks/callback_util.py +26 -16
  3. sapiopycommons/files/assay_plate_reader.py +93 -0
  4. sapiopycommons/files/file_text_converter.py +207 -0
  5. sapiopycommons/flowcyto/flow_cyto.py +2 -24
  6. sapiopycommons/general/accession_service.py +2 -28
  7. sapiopycommons/multimodal/multimodal.py +2 -24
  8. sapiopycommons/webhook/webservice_handlers.py +1 -1
  9. {sapiopycommons-2025.8.15a705.dist-info → sapiopycommons-2025.8.15a706.dist-info}/METADATA +2 -2
  10. {sapiopycommons-2025.8.15a705.dist-info → sapiopycommons-2025.8.15a706.dist-info}/RECORD +12 -43
  11. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.py +0 -43
  12. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.pyi +0 -31
  13. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2_grpc.py +0 -24
  14. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.py +0 -123
  15. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.pyi +0 -598
  16. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2_grpc.py +0 -24
  17. sapiopycommons/ai/api/plan/converter/proto/converter_pb2.py +0 -50
  18. sapiopycommons/ai/api/plan/converter/proto/converter_pb2.pyi +0 -61
  19. sapiopycommons/ai/api/plan/converter/proto/converter_pb2_grpc.py +0 -149
  20. sapiopycommons/ai/api/plan/item/proto/item_container_pb2.py +0 -55
  21. sapiopycommons/ai/api/plan/item/proto/item_container_pb2.pyi +0 -88
  22. sapiopycommons/ai/api/plan/item/proto/item_container_pb2_grpc.py +0 -24
  23. sapiopycommons/ai/api/plan/proto/step_output_pb2.py +0 -45
  24. sapiopycommons/ai/api/plan/proto/step_output_pb2.pyi +0 -42
  25. sapiopycommons/ai/api/plan/proto/step_output_pb2_grpc.py +0 -24
  26. sapiopycommons/ai/api/plan/proto/step_pb2.py +0 -43
  27. sapiopycommons/ai/api/plan/proto/step_pb2.pyi +0 -43
  28. sapiopycommons/ai/api/plan/proto/step_pb2_grpc.py +0 -24
  29. sapiopycommons/ai/api/plan/script/proto/script_pb2.py +0 -59
  30. sapiopycommons/ai/api/plan/script/proto/script_pb2.pyi +0 -102
  31. sapiopycommons/ai/api/plan/script/proto/script_pb2_grpc.py +0 -153
  32. sapiopycommons/ai/api/plan/tool/proto/entry_pb2.py +0 -41
  33. sapiopycommons/ai/api/plan/tool/proto/entry_pb2.pyi +0 -35
  34. sapiopycommons/ai/api/plan/tool/proto/entry_pb2_grpc.py +0 -24
  35. sapiopycommons/ai/api/plan/tool/proto/tool_pb2.py +0 -75
  36. sapiopycommons/ai/api/plan/tool/proto/tool_pb2.pyi +0 -237
  37. sapiopycommons/ai/api/plan/tool/proto/tool_pb2_grpc.py +0 -154
  38. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.py +0 -39
  39. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.pyi +0 -32
  40. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2_grpc.py +0 -24
  41. sapiopycommons/ai/converter_service_base.py +0 -116
  42. sapiopycommons/ai/protobuf_utils.py +0 -504
  43. sapiopycommons/ai/test_client.py +0 -338
  44. sapiopycommons/ai/tool_service_base.py +0 -917
  45. {sapiopycommons-2025.8.15a705.dist-info → sapiopycommons-2025.8.15a706.dist-info}/WHEEL +0 -0
  46. {sapiopycommons-2025.8.15a705.dist-info → sapiopycommons-2025.8.15a706.dist-info}/licenses/LICENSE +0 -0
@@ -1765,8 +1765,11 @@ class CallbackUtil:
1765
1765
  blank_result_handling = BlankResultHandling.REPEAT
1766
1766
  def not_blank_func(r: list[DataRecord]) -> bool:
1767
1767
  return bool(r)
1768
- return self.__send_dialog_blank_results(request, self.callback.show_input_selection_dialog, not_blank_func,
1769
- blank_result_handling, repeat_message, cancel_message)
1768
+ response: list[DataRecord] = self.__send_dialog_blank_results(request,
1769
+ self.callback.show_input_selection_dialog,
1770
+ not_blank_func, blank_result_handling,
1771
+ repeat_message, cancel_message)
1772
+ return self.rec_handler.wrap_models(response, wrapper_type)
1770
1773
 
1771
1774
  # FR-47690: Deprecated the require_authentication parameter.
1772
1775
  # noinspection PyUnusedLocal
@@ -1812,7 +1815,8 @@ class CallbackUtil:
1812
1815
  return response
1813
1816
 
1814
1817
  def request_file(self, title: str, exts: Iterable[str] | None = None,
1815
- show_image_editor: bool = False, show_camera_button: bool = False) -> tuple[str, bytes]:
1818
+ show_image_editor: bool = False, show_camera_button: bool = False,
1819
+ *, enforce_file_extensions: bool = True) -> tuple[str, bytes]:
1816
1820
  """
1817
1821
  Request a single file from the user.
1818
1822
 
@@ -1822,6 +1826,8 @@ class CallbackUtil:
1822
1826
  :param show_image_editor: Whether the user will see an image editor when image is uploaded in this file prompt.
1823
1827
  :param show_camera_button: Whether the user will be able to use camera to take a picture as an upload request,
1824
1828
  rather than selecting an existing file.
1829
+ :param enforce_file_extensions: If true, then the file extensions provided in the exts parameter will be
1830
+ enforced. If false, then the user may upload any file type.
1825
1831
  :return: The file name and bytes of the uploaded file.
1826
1832
  """
1827
1833
  # If no extensions were provided, use an empty list for the extensions instead.
@@ -1841,11 +1847,12 @@ class CallbackUtil:
1841
1847
  file_path: str = self.__send_dialog(request, self.callback.show_file_dialog, data_sink=do_consume)
1842
1848
 
1843
1849
  # Verify that each of the file given matches the expected extension(s).
1844
- self.__verify_file(file_path, sink.data, exts)
1850
+ self.__verify_file(file_path, sink.data, exts if enforce_file_extensions else None)
1845
1851
  return file_path, sink.data
1846
1852
 
1847
1853
  def request_files(self, title: str, exts: Iterable[str] | None = None,
1848
- show_image_editor: bool = False, show_camera_button: bool = False) -> dict[str, bytes]:
1854
+ show_image_editor: bool = False, show_camera_button: bool = False,
1855
+ *, enforce_file_extensions: bool = True) -> dict[str, bytes]:
1849
1856
  """
1850
1857
  Request multiple files from the user.
1851
1858
 
@@ -1855,6 +1862,8 @@ class CallbackUtil:
1855
1862
  :param show_image_editor: Whether the user will see an image editor when image is uploaded in this file prompt.
1856
1863
  :param show_camera_button: Whether the user will be able to use camera to take a picture as an upload request,
1857
1864
  rather than selecting an existing file.
1865
+ :param enforce_file_extensions: If true, then the file extensions provided in the exts parameter will be
1866
+ enforced. If false, then the user may upload any file type.
1858
1867
  :return: A dictionary of file name to file bytes for each file the user uploaded.
1859
1868
  """
1860
1869
  # If no extensions were provided, use an empty list for the extensions instead.
@@ -1870,7 +1879,7 @@ class CallbackUtil:
1870
1879
  for file_path in file_paths:
1871
1880
  sink = InMemoryRecordDataSink(self.user)
1872
1881
  sink.consume_client_callback_file_path_data(file_path)
1873
- self.__verify_file(file_path, sink.data, exts)
1882
+ self.__verify_file(file_path, sink.data, exts if enforce_file_extensions else None)
1874
1883
  ret_dict.update({file_path: sink.data})
1875
1884
 
1876
1885
  return ret_dict
@@ -1887,16 +1896,17 @@ class CallbackUtil:
1887
1896
  """
1888
1897
  if file_path is None or len(file_path) == 0 or file_bytes is None or len(file_bytes) == 0:
1889
1898
  raise SapioUserErrorException("Empty file provided or file unable to be read.")
1890
- if allowed_extensions:
1891
- matches: bool = False
1892
- for ext in allowed_extensions:
1893
- # FR-47690: Changed to a case-insensitive match.
1894
- if file_path.casefold().endswith("." + ext.lstrip(".").casefold()):
1895
- matches = True
1896
- break
1897
- if matches is False:
1898
- raise SapioUserErrorException("Unsupported file type. Expecting the following extension(s): "
1899
- + (",".join(allowed_extensions)))
1899
+ if not allowed_extensions:
1900
+ return
1901
+ matches: bool = False
1902
+ for ext in allowed_extensions:
1903
+ # FR-47690: Changed to a case-insensitive match.
1904
+ if file_path.casefold().endswith("." + ext.lstrip(".").casefold()):
1905
+ matches = True
1906
+ break
1907
+ if not matches:
1908
+ raise SapioUserErrorException("Unsupported file type. Expecting the following extension(s): "
1909
+ + (",".join(allowed_extensions)))
1900
1910
 
1901
1911
  def write_file(self, file_name: str, file_data: str | bytes) -> None:
1902
1912
  """
@@ -0,0 +1,93 @@
1
+ import base64
2
+ import dataclasses
3
+ from typing import Any
4
+
5
+ from databind.core.dataclasses import dataclass
6
+ from databind.json import loads
7
+ from sapiopylib.rest.utils.singletons import SapioContextManager
8
+
9
+
10
+ @dataclasses.dataclass
11
+ class ProcessAssayPlateRequest:
12
+ """
13
+ A request to process the results of assay plate reader with a configuration set in Sapio.
14
+
15
+ Attributes:
16
+ num_rows (int): The number of rows in the plate.
17
+ num_columns (int): The number of columns in the plate.
18
+ plate_ids_in_context (list[str]): List of plate IDs that are in context for this request.
19
+ filename (str): The name of the file containing the assay data.
20
+ file_data (bytes): The binary content of the file.
21
+ plate_reader_config_name (str): The name of the plate reader configuration to use.
22
+ """
23
+ num_rows: int
24
+ num_columns: int
25
+ plate_ids_in_context: list[str] | None
26
+ filename: str
27
+ file_data: bytes
28
+ plate_reader_config_name: str
29
+
30
+ def to_json(self) -> dict[str, Any]:
31
+ return {
32
+ "numRows": self.num_rows,
33
+ "numCols": self.num_columns,
34
+ "plateIdsInContext": self.plate_ids_in_context,
35
+ "fileName": self.filename,
36
+ "fileDataBase64": base64.b64encode(self.file_data).decode('utf-8'),
37
+ "plateReaderName": self.plate_reader_config_name
38
+ }
39
+
40
+
41
+ @dataclass
42
+ class AssayPlateResultIdent:
43
+ plateId: str
44
+ channelIdOrBlock: str
45
+ kineticAssaySeconds: float | None
46
+
47
+
48
+ @dataclass
49
+ class AssayResultDatum:
50
+ """
51
+ Describes the data received from an assay plate reader.
52
+ Most of the time, the data is a single value, but sometimes it can be multiple values, especially for kinetic data.
53
+ """
54
+ DEFAULT_PROPERTY_NAME: str = "read"
55
+ rowPosition: str
56
+ columnPosition: str
57
+ valueByPropertyName: dict[str, float]
58
+ textValueByPropertyName: dict[str, str]
59
+
60
+
61
+ @dataclass
62
+ class AssayPlateResult:
63
+ """
64
+ Assay plate load result for a single plate in a file. A file can have more than one of this result if it has multiple plate of data in a single file.
65
+ """
66
+ resultIdent: AssayPlateResultIdent
67
+ numRows: int
68
+ numColumns: int
69
+ resultDatum: list[AssayResultDatum]
70
+
71
+
72
+ @dataclass
73
+ class AssayFileLoadResult:
74
+ """
75
+ The entire top-level file loading result for an assay plate reader file.
76
+ """
77
+ filename: str
78
+ plateResultList: list[AssayPlateResult]
79
+
80
+
81
+ class AssayPlateReader(SapioContextManager):
82
+ """
83
+ This class contains services for Sapio Assay Plate Reader.
84
+ """
85
+
86
+ def process_plate_reader_data(self, request: ProcessAssayPlateRequest) -> AssayFileLoadResult:
87
+ """
88
+ Processes the assay plate reader data using provided request into a structured result using configuration defined in Sapio.
89
+ """
90
+ payload = request.to_json()
91
+ response = self.user.plugin_post("assayplatereader/process", payload=payload)
92
+ self.user.raise_for_status(response)
93
+ return loads(response.text, AssayFileLoadResult)
@@ -0,0 +1,207 @@
1
+ import io
2
+ import os
3
+ import tempfile
4
+ from enum import Enum, auto
5
+
6
+ class FileType(Enum):
7
+ """Supported file types for conversion."""
8
+ TXT = auto()
9
+ MD = auto()
10
+ CSV = auto()
11
+ DOC = auto()
12
+ DOCX = auto()
13
+ XLS = auto()
14
+ XLSX = auto()
15
+ PPT = auto()
16
+ PPTX = auto()
17
+ PDF = auto()
18
+ UNKNOWN = auto()
19
+
20
+
21
+ class FileToTextConverter:
22
+ """
23
+ A class for converting various file types to raw text.
24
+ """
25
+ @staticmethod
26
+ def mime_type_to_enum(mime_type: str) -> FileType:
27
+ """
28
+ Converts a MIME type to a FileType enum.
29
+
30
+ :param mime_type: The MIME type string to convert.
31
+ :return: The corresponding FileType enum, or UNKNOWN if not recognized.
32
+ """
33
+ if not mime_type or not mime_type.strip():
34
+ return FileType.UNKNOWN
35
+
36
+ mime_map = {
37
+ "text/plain": FileType.TXT,
38
+ "text/markdown": FileType.MD,
39
+ "text/csv": FileType.CSV,
40
+ "application/msword": FileType.DOC,
41
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": FileType.DOCX,
42
+ "application/vnd.ms-excel": FileType.XLS,
43
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": FileType.XLSX,
44
+ "application/vnd.ms-powerpoint": FileType.PPT,
45
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": FileType.PPTX,
46
+ "application/pdf": FileType.PDF,
47
+ }
48
+ return mime_map.get(mime_type, FileType.UNKNOWN)
49
+
50
+ @staticmethod
51
+ def file_extension_to_enum(file_path: str) -> FileType:
52
+ """
53
+ Converts a file path or extension to a FileType enum.
54
+
55
+ :param file_path: The file path or extension to convert.
56
+ :return: The corresponding FileType enum, or UNKNOWN if not recognized.
57
+ """
58
+ if not file_path or not file_path.strip():
59
+ return FileType.UNKNOWN
60
+
61
+ # Extract the file extension, removing the leading dot and making it lowercase
62
+ file_extension = os.path.splitext(file_path)[1].lstrip('.').lower()
63
+
64
+ ext_map = {
65
+ "txt": FileType.TXT,
66
+ "md": FileType.MD,
67
+ "csv": FileType.CSV,
68
+ "doc": FileType.DOC,
69
+ "docx": FileType.DOCX,
70
+ "xls": FileType.XLS,
71
+ "xlsx": FileType.XLSX,
72
+ "ppt": FileType.PPT,
73
+ "pptx": FileType.PPTX,
74
+ "pdf": FileType.PDF,
75
+ }
76
+ return ext_map.get(file_extension, FileType.UNKNOWN)
77
+
78
+ @classmethod
79
+ def parse_file(cls, file_type: FileType, file_bytes: bytes) -> str | None:
80
+ """
81
+ Parses file bytes based on the FileType and returns the text content.
82
+
83
+ :param file_type: The type of the file to parse.
84
+ :param file_bytes: The raw bytes of the file to parse.
85
+ :return: The text content of the file, or None if the file type is not supported or parsing fails.
86
+ """
87
+ if file_type is None or file_bytes is None:
88
+ return None
89
+ if not file_bytes:
90
+ return ""
91
+
92
+ # Dispatch to the correct parser method
93
+ parser_map = {
94
+ FileType.TXT: cls._parse_plain_text,
95
+ FileType.MD: cls._parse_plain_text,
96
+ FileType.CSV: cls._parse_plain_text,
97
+ FileType.DOC: cls._parse_doc,
98
+ FileType.DOCX: cls._parse_docx,
99
+ FileType.XLS: cls._parse_xls,
100
+ FileType.XLSX: cls._parse_xlsx,
101
+ FileType.PPT: cls._parse_ppt,
102
+ FileType.PPTX: cls._parse_pptx,
103
+ FileType.PDF: cls._parse_pdf,
104
+ }
105
+
106
+ parser_func = parser_map.get(file_type)
107
+
108
+ if parser_func:
109
+ return parser_func(file_bytes)
110
+
111
+ return None
112
+
113
+ @staticmethod
114
+ def _parse_plain_text(file_bytes: bytes) -> str:
115
+ return file_bytes.decode('utf-8')
116
+
117
+ @staticmethod
118
+ def _run_textract(file_bytes: bytes, extension: str) -> str:
119
+ """
120
+ Helper to run textract on in-memory bytes by writing to a temp file.
121
+ Note: textract may require external system dependencies.
122
+ """
123
+ import textract
124
+ with tempfile.NamedTemporaryFile(suffix=f".{extension}", delete=True) as temp_file:
125
+ temp_file.write(file_bytes)
126
+ temp_file.flush() # Ensure all bytes are written to disk
127
+ text = textract.process(temp_file.name).decode('utf-8')
128
+ return text
129
+
130
+ @classmethod
131
+ def _parse_doc(cls, file_bytes: bytes) -> str:
132
+ return cls._run_textract(file_bytes, 'doc')
133
+
134
+ @staticmethod
135
+ def _parse_docx(file_bytes: bytes) -> str:
136
+ import docx
137
+ with io.BytesIO(file_bytes) as stream:
138
+ document = docx.Document(stream)
139
+ return "\n".join(para.text for para in document.paragraphs if para.text.strip())
140
+
141
+ @staticmethod
142
+ def _parse_xls(file_bytes: bytes) -> str:
143
+ import xlrd
144
+ workbook = xlrd.open_workbook(file_contents=file_bytes)
145
+ text_parts = []
146
+ for sheet in workbook.sheets():
147
+ text_parts.append(f"Sheet: {sheet.name}\n")
148
+ for row_idx in range(sheet.nrows):
149
+ row_cells = []
150
+ for col_idx in range(sheet.ncols):
151
+ cell_text = str(sheet.cell_value(row_idx, col_idx))
152
+ if cell_text.strip():
153
+ row_cells.append(cell_text + "\t")
154
+ if row_cells:
155
+ text_parts.append("".join(row_cells))
156
+ text_parts.append("\n")
157
+ text_parts.append("\n")
158
+ return "".join(text_parts)
159
+
160
+ @staticmethod
161
+ def _parse_xlsx(file_bytes: bytes) -> str:
162
+ import openpyxl
163
+ with io.BytesIO(file_bytes) as stream:
164
+ workbook = openpyxl.load_workbook(stream, read_only=True)
165
+ text_parts = []
166
+ for sheet in workbook.worksheets:
167
+ text_parts.append(f"Sheet: {sheet.title}\n")
168
+ for row in sheet.iter_rows():
169
+ row_cells = []
170
+ for cell in row:
171
+ cell_text = str(cell.value) if cell.value is not None else ""
172
+ if cell_text.strip():
173
+ row_cells.append(cell_text + "\t")
174
+ if row_cells:
175
+ text_parts.append("".join(row_cells))
176
+ text_parts.append("\n")
177
+ text_parts.append("\n")
178
+ return "".join(text_parts)
179
+
180
+ @classmethod
181
+ def _parse_ppt(cls, file_bytes: bytes) -> str:
182
+ return cls._run_textract(file_bytes, 'ppt')
183
+
184
+ @staticmethod
185
+ def _parse_pptx(file_bytes: bytes) -> str:
186
+ import pptx
187
+ with io.BytesIO(file_bytes) as stream:
188
+ presentation = pptx.Presentation(stream)
189
+ text_parts = []
190
+ for slide in presentation.slides:
191
+ for shape in slide.shapes:
192
+ if shape.has_text_frame:
193
+ text = shape.text_frame.text
194
+ if text and text.strip():
195
+ text_parts.append(text)
196
+ return "\n".join(text_parts)
197
+
198
+ @staticmethod
199
+ def _parse_pdf(file_bytes: bytes) -> str:
200
+ """Parses a PDF file's bytes and extracts text using PyMuPDF."""
201
+ import pymupdf
202
+ text_parts = []
203
+ with io.BytesIO(file_bytes) as stream:
204
+ with pymupdf.open(stream=stream) as doc:
205
+ for page in doc:
206
+ text_parts.append(page.get_text())
207
+ return "\n".join(text_parts)
@@ -4,38 +4,16 @@ from weakref import WeakValueDictionary
4
4
 
5
5
  from databind.json import dumps
6
6
  from sapiopylib.rest.User import SapioUser
7
+ from sapiopylib.rest.utils.singletons import SapioContextManager
7
8
 
8
9
  from sapiopycommons.flowcyto.flowcyto_data import FlowJoWorkspaceInputJson, UploadFCSInputJson, \
9
10
  ComputeFlowStatisticsInputJson
10
11
 
11
12
 
12
- class FlowCytoManager:
13
+ class FlowCytoManager(SapioContextManager):
13
14
  """
14
15
  This manager includes flow cytometry analysis tools that would require FlowCyto license to use.
15
16
  """
16
- _user: SapioUser
17
-
18
- __instances: WeakValueDictionary[SapioUser, FlowCytoManager] = WeakValueDictionary()
19
- __initialized: bool
20
-
21
- def __new__(cls, user: SapioUser):
22
- """
23
- Observes singleton pattern per record model manager object.
24
-
25
- :param user: The user that will make the webservice request to the application.
26
- """
27
- obj = cls.__instances.get(user)
28
- if not obj:
29
- obj = object.__new__(cls)
30
- obj.__initialized = False
31
- cls.__instances[user] = obj
32
- return obj
33
-
34
- def __init__(self, user: SapioUser):
35
- if self.__initialized:
36
- return
37
- self._user = user
38
- self.__initialized = True
39
17
 
40
18
  def create_flowjo_workspace(self, workspace_input: FlowJoWorkspaceInputJson) -> int:
41
19
  """
@@ -5,6 +5,7 @@ from typing import Any
5
5
  from weakref import WeakValueDictionary
6
6
 
7
7
  from sapiopylib.rest.User import SapioUser
8
+ from sapiopylib.rest.utils.singletons import SapioContextManager
8
9
 
9
10
  _STR_JAVA_TYPE = "java.lang.String"
10
11
  _INT_JAVA_TYPE = "java.lang.Integer"
@@ -274,37 +275,10 @@ class AccessionServiceDescriptor:
274
275
  }
275
276
 
276
277
 
277
- class AccessionService:
278
+ class AccessionService(SapioContextManager):
278
279
  """
279
280
  Provides Sapio Foundations Accession Service functionalities.
280
281
  """
281
- _user: SapioUser
282
-
283
- __instances: WeakValueDictionary[SapioUser, AccessionService] = WeakValueDictionary()
284
- __initialized: bool
285
-
286
- @property
287
- def user(self) -> SapioUser:
288
- return self._user
289
-
290
- def __new__(cls, user: SapioUser):
291
- """
292
- Observes singleton pattern per record model manager object.
293
-
294
- :param user: The user that will make the webservice request to the application.
295
- """
296
- obj = cls.__instances.get(user)
297
- if not obj:
298
- obj = object.__new__(cls)
299
- obj.__initialized = False
300
- cls.__instances[user] = obj
301
- return obj
302
-
303
- def __init__(self, user: SapioUser):
304
- if self.__initialized:
305
- return
306
- self._user = user
307
- self.__initialized = True
308
282
 
309
283
  def accession_with_config(self, data_type_name: str, data_field_name: str, num_ids: int) -> list[str]:
310
284
  """
@@ -7,35 +7,13 @@ from weakref import WeakValueDictionary
7
7
  from databind.json import dumps, loads
8
8
  from sapiopylib.rest.User import SapioUser
9
9
  from sapiopylib.rest.pojo.DataRecord import DataRecord
10
+ from sapiopylib.rest.utils.singletons import SapioContextManager
10
11
 
11
12
  from sapiopycommons.general.exceptions import SapioException
12
13
  from sapiopycommons.multimodal.multimodal_data import *
13
14
 
14
15
 
15
- class MultiModalManager:
16
- _user: SapioUser
17
-
18
- __instances: WeakValueDictionary[SapioUser, MultiModalManager] = WeakValueDictionary()
19
- __initialized: bool
20
-
21
- def __new__(cls, user: SapioUser):
22
- """
23
- Observes singleton pattern per record model manager object.
24
-
25
- :param user: The user that will make the webservice request to the application.
26
- """
27
- obj = cls.__instances.get(user)
28
- if not obj:
29
- obj = object.__new__(cls)
30
- obj.__initialized = False
31
- cls.__instances[user] = obj
32
- return obj
33
-
34
- def __init__(self, user:SapioUser):
35
- if self.__initialized:
36
- return
37
- self._user = user
38
- self.__initialized = True
16
+ class MultiModalManager(SapioContextManager):
39
17
 
40
18
  def load_image_data(self, request: ImageDataRequestPojo) -> list[str]:
41
19
  """
@@ -140,7 +140,7 @@ class AbstractWebserviceHandler(AbstractWebhookHandler):
140
140
  # Get the login credentials from the headers.
141
141
  auth: str = headers.get("Authorization")
142
142
  if auth and auth.startswith("Basic "):
143
- credentials: list[str] = b64decode(auth.split("Basic ")[1]).decode().split(":", 1)
143
+ credentials: list[str] = b64decode(auth.split("Basic ")[1]).decode().split(":")
144
144
  user = self.basic_auth(url, credentials[0], credentials[1])
145
145
  elif auth and auth.startswith("Bearer "):
146
146
  user = self.bearer_token_auth(url, auth.split("Bearer ")[1])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2025.8.15a705
3
+ Version: 2025.8.15a706
4
4
  Summary: Official Sapio Python API Utilities Package
5
5
  Project-URL: Homepage, https://github.com/sapiosciences
6
6
  Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
18
  Requires-Python: >=3.10
19
19
  Requires-Dist: databind>=4.5
20
- Requires-Dist: sapiopylib>=2025.4.17.264
20
+ Requires-Dist: sapiopylib>=2025.7.31a279
21
21
  Description-Content-Type: text/markdown
22
22
 
23
23
 
@@ -1,41 +1,8 @@
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=w4_PXjlk7_F5AUMlPUhKJ9gZlxY9RNG6yH9cDDFROuo,4620
4
- sapiopycommons/ai/protobuf_utils.py,sha256=QdYcelIvOIKBV5u_KR_Fh06kZ0eJDIb5ZVnfqV01FeE,24601
5
- sapiopycommons/ai/test_client.py,sha256=CBacWKFVLdrStxuyRnJP5ifDen5dpvkgdF9mz-_vlu4,14397
6
- sapiopycommons/ai/tool_service_base.py,sha256=kfMWU0OndYl0XnKwztHGTT-L20Mpq7VUI-vhwyfJkkI,43375
7
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.py,sha256=YcZjb_YM-XeLErM8hEC_S7vGMVGvcXAMGs2b-u5zvOE,2377
8
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.pyi,sha256=FwtXmNAf7iYGEFm4kbqb04v77jNHbZg18ZmEDhle_bU,1444
9
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2_grpc.py,sha256=wPImJPdCUZNVEVoUWzsba9kGIXjEKPdUkawP5SnVyiU,932
10
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.py,sha256=nWC91vR2pMgMUyNOZRZ0YiuL1-8ntnjXLqt1daxsD34,20869
11
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.pyi,sha256=U5zXrbBxsWilLTsRWJd1TqjdjLKFsr3enF9OJ8GfyWw,34028
12
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2_grpc.py,sha256=4vD4jWanaJ4uclSkFmS7JIz_lwYXDWBE3DomuPjUyII,941
13
- sapiopycommons/ai/api/plan/converter/proto/converter_pb2.py,sha256=dT8X_D18RRAMY5PA7tHVGlbwhH2JIH6CrkuXb6iwL4E,3401
14
- sapiopycommons/ai/api/plan/converter/proto/converter_pb2.pyi,sha256=LazSPiQ0fCmI_lZ5lm-871XMU-PTlAtfbjta8Aqp3Mk,4104
15
- sapiopycommons/ai/api/plan/converter/proto/converter_pb2_grpc.py,sha256=EvhojTeQHuLNrK1HVolQbcOsa-sJDM8ExLqqu4F_a6U,6515
16
- sapiopycommons/ai/api/plan/item/proto/item_container_pb2.py,sha256=qph1v5-16l_87aKH-aQixGjQthg_xXF7Lm66DCfFWIA,3822
17
- sapiopycommons/ai/api/plan/item/proto/item_container_pb2.pyi,sha256=K8Y0dvNY7V0MLOegC1ODXqc1g4lH1NY70EGH2UWN9vQ,4118
18
- sapiopycommons/ai/api/plan/item/proto/item_container_pb2_grpc.py,sha256=nNTWRJEg7MglgC1AsVBsdXo0Q1sYvvsucwX3KpzYiOw,933
19
- sapiopycommons/ai/api/plan/proto/step_output_pb2.py,sha256=JpBZSyoYyPTEaaXjW664PeJNK0zxV1mly_kp5re42z4,2661
20
- sapiopycommons/ai/api/plan/proto/step_output_pb2.pyi,sha256=yuxOYnDZ9DRuu-TLzaKOW_B4LUiYxTrNc2AbssXg4kE,2022
21
- sapiopycommons/ai/api/plan/proto/step_output_pb2_grpc.py,sha256=vDRY_pIIshQ4UpdW-ra1F5zBmntdsW2scySkMAA-zfc,925
22
- sapiopycommons/ai/api/plan/proto/step_pb2.py,sha256=nL976oTFdX4ih4gg7_J-8eFoGB69tFvERB2gT3L2-6s,2439
23
- sapiopycommons/ai/api/plan/proto/step_pb2.pyi,sha256=QPIcsjcUvEGQkdZMUMiVzFFNDl8yOUe_qJtf5XEp5Ck,2062
24
- sapiopycommons/ai/api/plan/proto/step_pb2_grpc.py,sha256=DgiBYFvTNiDG_2a9Tpt5iel2fRUfePZWP41fZTC-KWk,918
25
- sapiopycommons/ai/api/plan/script/proto/script_pb2.py,sha256=iUjALwM3e-mwuwVTc_N0k9CF4KkwniIPcxLiQK3fOR0,5200
26
- sapiopycommons/ai/api/plan/script/proto/script_pb2.pyi,sha256=IsaOnZVLD91PQwQcjDxMpfcYlI6U9cwEIMl770m6HgI,6204
27
- sapiopycommons/ai/api/plan/script/proto/script_pb2_grpc.py,sha256=RkShHpe_d5EJHk3qp-or1JpvSEqShb7cCiaXnJ2YSww,6931
28
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2.py,sha256=2q87XYhq_SOADuhoIyr1CHQC3SZiupjSjJLNp1np-hA,2141
29
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2.pyi,sha256=R5apvEaENIKKG2DbqwxjgI9arkqYq928xe04WNtTniM,2365
30
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2_grpc.py,sha256=YKkX2kexERUx4asLCShufSnZhgf339Zk8Xw1FAgLfHQ,924
31
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2.py,sha256=WzE4-S1KXJC26RlubLkiDa3GnEhZ0pbxE3V4AuFicZM,8255
32
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2.pyi,sha256=YU7CJkij037QJNR8VhheC-ouukUD13vyLt18YXR1HYA,16763
33
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2_grpc.py,sha256=68u5E1ZKha5frP5GuY8Ad-c0c79vBhtfnj5Q4u-8xOY,6982
34
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.py,sha256=WKzNi-d5dqeJbmEXUVE5qJ4Qm34HmsqRXRtXih382g8,2100
35
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.pyi,sha256=vLYA8Tkzq2AwgVadoUp5vAg4HgGlgga0kzeS3e_XkCQ,1621
36
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2_grpc.py,sha256=2W0YzT4SfnGTLq98AdvkyNM0n75Tkl8DervPS1ryGao,932
3
+ sapiopycommons/ai/tool_of_tools.py,sha256=zYmQ4rNX-qYQnc-vNDnYZjtv9JgmQAmVVuHfVOdBF3w,46984
37
4
  sapiopycommons/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- sapiopycommons/callbacks/callback_util.py,sha256=OuPJ1o6jcDQ7qV-dxrjAkJerGbVI9_9P-xu0r3ODaMM,153008
5
+ sapiopycommons/callbacks/callback_util.py,sha256=Z1LcXnRRjXyhmcSDUwh4NzcA6ICtcbFUMKcvAqQcS8E,153811
39
6
  sapiopycommons/callbacks/field_builder.py,sha256=rnIP-RJafk3mZlAx1eJ8a0eSW9Ps_L6_WadCmusnENw,38772
40
7
  sapiopycommons/chem/IndigoMolecules.py,sha256=7ucCaRMLu1zfH2uPIvXwRTSdpNcS03O1P9p_O-5B4xQ,5110
41
8
  sapiopycommons/chem/Molecules.py,sha256=mVqPn32MPMjF0iZas-5MFkS-upIdoW5OB72KKZmJRJA,12523
@@ -58,17 +25,19 @@ sapiopycommons/eln/experiment_tags.py,sha256=7-fpOiSqrjbXmWIJhEhaxMgLsVCPAtKqH8x
58
25
  sapiopycommons/eln/plate_designer.py,sha256=XFazSvhTbSy47t80-jc2tyx_-fQ_IUjKd18JQKEFcsY,13939
59
26
  sapiopycommons/eln/step_creation.py,sha256=CFkGC-SxwAQpQlcs_obqLAVgmsNxKSGMqMtO_E6IVmw,10171
60
27
  sapiopycommons/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
+ sapiopycommons/files/assay_plate_reader.py,sha256=3c2PQiiAbc2QJU9ZfNLzcTmvJrUwsbkIHO7R6R52xGU,3020
61
29
  sapiopycommons/files/complex_data_loader.py,sha256=T39veNhvYl6j_uZjIIJ8Mk5Aa7otR5RB-g8XlAdkksA,1421
62
30
  sapiopycommons/files/file_bridge.py,sha256=vKbqxPexi15epr_-_qLrEfYoxNxB031mXN92iVtOMqE,9511
63
31
  sapiopycommons/files/file_bridge_handler.py,sha256=SEYDIQhSCmjI6qyLdDJE8JVKSd0WYvF7JvAq_Ahp9Do,25503
64
32
  sapiopycommons/files/file_data_handler.py,sha256=f96MlkMuQhUCi4oLnzJK5AiuElCp5jLI8_sJkZVwpws,36779
33
+ sapiopycommons/files/file_text_converter.py,sha256=Gaj_divTiKXWd6flDOgrxNXpcn9fDWqxX6LUG0joePk,7516
65
34
  sapiopycommons/files/file_util.py,sha256=djouyGjsYgWzjz2OBRnSeMDgj6NrsJUm1a2J93J8Wco,31915
66
35
  sapiopycommons/files/file_validator.py,sha256=ryg22-93csmRO_Pv0ZpWphNkB74xWZnHyJ23K56qLj0,28761
67
36
  sapiopycommons/files/file_writer.py,sha256=hACVl0duCjP28gJ1NPljkjagNCLod0ygUlPbvUmRDNM,17605
68
- sapiopycommons/flowcyto/flow_cyto.py,sha256=vs9WhXXKz3urpjL8QKSk56B-NSmQR3O3x_WFBKoeO10,3227
37
+ sapiopycommons/flowcyto/flow_cyto.py,sha256=B6DFquLi-gcWfJWyP4vYfwTXXJKl6O9W5-k8FzkM0Oo,2610
69
38
  sapiopycommons/flowcyto/flowcyto_data.py,sha256=mYKFuLbtpJ-EsQxLGtu4tNHVlygTxKixgJxJqD68F58,2596
70
39
  sapiopycommons/general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
- sapiopycommons/general/accession_service.py,sha256=3e__bVs7CYZ1CduLlGA9plnK7nCtdy7GXjCrNObPFgo,13484
40
+ sapiopycommons/general/accession_service.py,sha256=ZvtvZg7d_siMJUedjrF14mcqo5ZqVA5IJxDa5enlB-8,12792
72
41
  sapiopycommons/general/aliases.py,sha256=VwnWf_P803pcteoAIs0DkLScVChCS5XNgryTp8FzaNc,14696
73
42
  sapiopycommons/general/audit_log.py,sha256=sQAMcJx0cNkgZm7nTZSaGPxWvHG0_x6dBtU0jESavb4,9131
74
43
  sapiopycommons/general/custom_report_util.py,sha256=9elLEUSgfM0gli8nRPz1uYkhaXN4Vnx3piSiNHv5IBs,19156
@@ -80,7 +49,7 @@ sapiopycommons/general/popup_util.py,sha256=HKILegU1uCL_6abNlNL0Wn3xgX2JNa_kJeq7
80
49
  sapiopycommons/general/sapio_links.py,sha256=YkcVKNLrSGoM7tCCXBAsIbIxylctwdcEyhePrRMODe0,2859
81
50
  sapiopycommons/general/storage_util.py,sha256=ovmK_jN7v09BoX07XxwShpBUC5WYQOM7dbKV_VeLXJU,8892
82
51
  sapiopycommons/general/time_util.py,sha256=jU1urPoZRv6evNucR0-288EyT4PrsDpCr-H1-7BKq9A,12363
83
- sapiopycommons/multimodal/multimodal.py,sha256=PFaGJPbKvW__tnxb8KkgkJZOKjQdgxF_kGfD5chet1s,6779
52
+ sapiopycommons/multimodal/multimodal.py,sha256=EP9WYzx1CvidmEBlvzO6tiF4HJwsPB1FgxpnbWzxnpA,6161
84
53
  sapiopycommons/multimodal/multimodal_data.py,sha256=0BeVPr9HaC0hNTF1v1phTIKGruvNnwerHsD994qJKBg,15099
85
54
  sapiopycommons/processtracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
86
55
  sapiopycommons/processtracking/custom_workflow_handler.py,sha256=eYKdYlwo8xx-6AkB_iPUBNV9yDoNvW2h_Sm3i8JpmRU,25844
@@ -96,8 +65,8 @@ sapiopycommons/sftpconnect/sftp_builder.py,sha256=lFK3FeXk-sFLefW0hqY8WGUQDeYiGa
96
65
  sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
66
  sapiopycommons/webhook/webhook_context.py,sha256=D793uLsb1691SalaPnBUk3rOSxn_hYLhdvkaIxjNXss,1909
98
67
  sapiopycommons/webhook/webhook_handlers.py,sha256=7o_wXOruhT9auNh8OfhJAh4WhhiPKij67FMBSpGPICc,39939
99
- sapiopycommons/webhook/webservice_handlers.py,sha256=cvW6Mk_110BzYqkbk63Kg7jWrltBCDALOlkJRu8h4VQ,14300
100
- sapiopycommons-2025.8.15a705.dist-info/METADATA,sha256=jzsSGPS3vU0kRu46cYHefEm0kFnErZgXJ_jQn8iq2PI,3143
101
- sapiopycommons-2025.8.15a705.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
102
- sapiopycommons-2025.8.15a705.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
103
- sapiopycommons-2025.8.15a705.dist-info/RECORD,,
68
+ sapiopycommons/webhook/webservice_handlers.py,sha256=tyaYGG1-v_JJrJHZ6cy5mGCxX9z1foLw7pM4MDJlFxs,14297
69
+ sapiopycommons-2025.8.15a706.dist-info/METADATA,sha256=N1Uw2VaZBEto5tJuF5nv67V5cu7HNRf06CWU3VUrQfM,3143
70
+ sapiopycommons-2025.8.15a706.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
71
+ sapiopycommons-2025.8.15a706.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
72
+ sapiopycommons-2025.8.15a706.dist-info/RECORD,,