sapiopycommons 2025.5.6a511__py3-none-any.whl → 2025.5.6a512__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 (54) hide show
  1. sapiopycommons/ai/__init__.py +0 -0
  2. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.py +43 -0
  3. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.pyi +31 -0
  4. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2_grpc.py +24 -0
  5. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.py +123 -0
  6. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.pyi +598 -0
  7. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2_grpc.py +24 -0
  8. sapiopycommons/ai/api/plan/proto/step_output_pb2.py +45 -0
  9. sapiopycommons/ai/api/plan/proto/step_output_pb2.pyi +42 -0
  10. sapiopycommons/ai/api/plan/proto/step_output_pb2_grpc.py +24 -0
  11. sapiopycommons/ai/api/plan/proto/step_pb2.py +43 -0
  12. sapiopycommons/ai/api/plan/proto/step_pb2.pyi +43 -0
  13. sapiopycommons/ai/api/plan/proto/step_pb2_grpc.py +24 -0
  14. sapiopycommons/ai/api/plan/script/proto/script_pb2.py +53 -0
  15. sapiopycommons/ai/api/plan/script/proto/script_pb2.pyi +99 -0
  16. sapiopycommons/ai/api/plan/script/proto/script_pb2_grpc.py +153 -0
  17. sapiopycommons/ai/api/plan/tool/proto/entry_pb2.py +57 -0
  18. sapiopycommons/ai/api/plan/tool/proto/entry_pb2.pyi +96 -0
  19. sapiopycommons/ai/api/plan/tool/proto/entry_pb2_grpc.py +24 -0
  20. sapiopycommons/ai/api/plan/tool/proto/tool_pb2.py +67 -0
  21. sapiopycommons/ai/api/plan/tool/proto/tool_pb2.pyi +220 -0
  22. sapiopycommons/ai/api/plan/tool/proto/tool_pb2_grpc.py +154 -0
  23. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.py +39 -0
  24. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.pyi +32 -0
  25. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2_grpc.py +24 -0
  26. sapiopycommons/ai/protobuf_utils.py +454 -0
  27. sapiopycommons/ai/tool_service_base.py +708 -0
  28. sapiopycommons/callbacks/callback_util.py +64 -116
  29. sapiopycommons/callbacks/field_builder.py +0 -2
  30. sapiopycommons/customreport/auto_pagers.py +1 -2
  31. sapiopycommons/customreport/term_builder.py +1 -1
  32. sapiopycommons/datatype/pseudo_data_types.py +326 -349
  33. sapiopycommons/eln/experiment_handler.py +719 -336
  34. sapiopycommons/eln/plate_designer.py +2 -7
  35. sapiopycommons/files/file_util.py +4 -4
  36. sapiopycommons/general/accession_service.py +2 -2
  37. sapiopycommons/general/aliases.py +1 -4
  38. sapiopycommons/general/html_formatter.py +456 -0
  39. sapiopycommons/general/sapio_links.py +12 -4
  40. sapiopycommons/processtracking/custom_workflow_handler.py +1 -2
  41. sapiopycommons/recordmodel/record_handler.py +27 -357
  42. sapiopycommons/rules/eln_rule_handler.py +1 -8
  43. sapiopycommons/rules/on_save_rule_handler.py +1 -8
  44. sapiopycommons/webhook/webhook_handlers.py +0 -3
  45. sapiopycommons/webhook/webservice_handlers.py +2 -2
  46. {sapiopycommons-2025.5.6a511.dist-info → sapiopycommons-2025.5.6a512.dist-info}/METADATA +2 -2
  47. sapiopycommons-2025.5.6a512.dist-info/RECORD +91 -0
  48. sapiopycommons/eln/experiment_cache.py +0 -188
  49. sapiopycommons/eln/experiment_step_factory.py +0 -476
  50. sapiopycommons/eln/step_creation.py +0 -236
  51. sapiopycommons/general/data_structure_util.py +0 -115
  52. sapiopycommons-2025.5.6a511.dist-info/RECORD +0 -67
  53. {sapiopycommons-2025.5.6a511.dist-info → sapiopycommons-2025.5.6a512.dist-info}/WHEEL +0 -0
  54. {sapiopycommons-2025.5.6a511.dist-info → sapiopycommons-2025.5.6a512.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,708 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import traceback
5
+ from abc import abstractmethod, ABC
6
+ from typing import Any, Iterable, Sequence
7
+
8
+ from grpc import ServicerContext
9
+ from sapiopylib.rest.User import SapioUser
10
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition
11
+
12
+ from sapiopycommons.ai.api.fielddefinitions.proto.fields_pb2 import FieldValueMapPbo, FieldValuePbo
13
+ from sapiopycommons.ai.api.fielddefinitions.proto.velox_field_def_pb2 import VeloxFieldDefPbo, FieldTypePbo, \
14
+ SelectionPropertiesPbo, IntegerPropertiesPbo, DoublePropertiesPbo, BooleanPropertiesPbo
15
+ from sapiopycommons.ai.api.plan.tool.proto.entry_pb2 import StepOutputBatchPbo, StepItemContainerPbo, DataTypePbo, \
16
+ StepBinaryContainerPbo, StepCsvContainerPbo, StepCsvHeaderRowPbo, StepCsvRowPbo, StepImageContainerPbo, \
17
+ StepJsonContainerPbo, StepTextContainerPbo, StepInputBatchPbo
18
+ from sapiopycommons.ai.api.plan.tool.proto.tool_pb2 import ToolDetailsRequestPbo, ToolDetailsResponsePbo, \
19
+ ToolDetailsPbo, ProcessStepRequestPbo, ProcessStepResponsePbo, ToolOutputDetailsPbo, ToolIoConfigBasePbo, \
20
+ ToolInputDetailsPbo
21
+ from sapiopycommons.ai.api.plan.tool.proto.tool_pb2_grpc import ToolServiceServicer
22
+ from sapiopycommons.ai.api.session.proto.sapio_conn_info_pb2 import SapioUserSecretTypePbo, SapioConnectionInfoPbo
23
+ from sapiopycommons.ai.protobuf_utils import ProtobufUtils
24
+ from sapiopycommons.general.aliases import FieldMap, FieldValue
25
+
26
+
27
+ class SapioToolResult(ABC):
28
+ """
29
+ A class representing a result from a Sapio tool. Instantiate one of the subclasses to create a result object.
30
+ """
31
+
32
+ @abstractmethod
33
+ def to_proto(self) -> StepOutputBatchPbo | list[FieldValueMapPbo]:
34
+ """
35
+ Convert this SapioToolResult object to a StepOutputBatchPbo or list of FieldValueMapPbo proto objects.
36
+ """
37
+ pass
38
+
39
+
40
+ class BinaryResult(SapioToolResult):
41
+ """
42
+ A class representing binary results from a Sapio tool.
43
+ """
44
+ binary_data: list[bytes]
45
+
46
+ def __init__(self, binary_data: list[bytes]):
47
+ """
48
+ :param binary_data: The binary data as a list of bytes.
49
+ """
50
+ self.binary_data = binary_data
51
+
52
+ def to_proto(self) -> StepOutputBatchPbo | list[FieldValueMapPbo]:
53
+ return StepOutputBatchPbo(
54
+ item_container=StepItemContainerPbo(
55
+ dataType=DataTypePbo.BINARY,
56
+ binary_container=StepBinaryContainerPbo(items=self.binary_data)
57
+ )
58
+ )
59
+
60
+
61
+ class CsvResult(SapioToolResult):
62
+ """
63
+ A class representing CSV results from a Sapio tool.
64
+ """
65
+ csv_data: list[dict[str, Any]]
66
+
67
+ def __init__(self, csv_data: list[dict[str, Any]]):
68
+ """
69
+ :param csv_data: The list of CSV data results, provided as a list of dictionaries of column name to value.
70
+ """
71
+ self.csv_data = csv_data
72
+
73
+ def to_proto(self) -> StepOutputBatchPbo | list[FieldValueMapPbo]:
74
+ return StepOutputBatchPbo(
75
+ item_container=StepItemContainerPbo(
76
+ dataType=DataTypePbo.CSV,
77
+ csv_container=StepCsvContainerPbo(
78
+ header=StepCsvHeaderRowPbo(cells=self.csv_data[0].keys()),
79
+ items=[StepCsvRowPbo(cells=[str(x) for x in row.values()]) for row in self.csv_data]
80
+ )
81
+ ) if self.csv_data else None
82
+ )
83
+
84
+
85
+ class FieldMapResult(SapioToolResult):
86
+ """
87
+ A class representing field map results from a Sapio tool.
88
+ """
89
+ field_maps: list[FieldMap]
90
+
91
+ def __init__(self, field_maps: list[FieldMap]):
92
+ """
93
+ :param field_maps: A list of field maps, where each map is a dictionary of field names to values. Each entry
94
+ will create a new data record in the system, so long as the tool definition specifies an output data type
95
+ name.
96
+ """
97
+ self.field_maps = field_maps
98
+
99
+ def to_proto(self) -> StepOutputBatchPbo | list[FieldValueMapPbo]:
100
+ new_records: list[FieldValueMapPbo] = []
101
+ for field_map in self.field_maps:
102
+ fields: dict[str, FieldValuePbo] = {}
103
+ for field, value in field_map.items():
104
+ field_value = FieldValuePbo()
105
+ if isinstance(value, str):
106
+ field_value.string_value = value
107
+ elif isinstance(value, int):
108
+ field_value.int_value = value
109
+ elif isinstance(value, float):
110
+ field_value.double_value = value
111
+ elif isinstance(value, bool):
112
+ field_value.bool_value = value
113
+ fields[field] = field_value
114
+ new_records.append(FieldValueMapPbo(fields=fields))
115
+ return new_records
116
+
117
+
118
+ class ImageResult(SapioToolResult):
119
+ """
120
+ A class representing image results from a Sapio tool.
121
+ """
122
+ image_format: str
123
+ image_data: list[bytes]
124
+
125
+ def __init__(self, image_format: str, image_data: list[bytes]):
126
+ """
127
+ :param image_format: The format of the image (e.g., PNG, JPEG).
128
+ :param image_data: The image data as a list of bytes. Each entry in the list represents a separate image.
129
+ """
130
+ self.image_format = image_format
131
+ self.image_data = image_data
132
+
133
+ def to_proto(self) -> StepOutputBatchPbo | list[FieldValueMapPbo]:
134
+ return StepOutputBatchPbo(
135
+ item_container=StepItemContainerPbo(
136
+ dataType=DataTypePbo.IMAGE,
137
+ image_container=StepImageContainerPbo(
138
+ image_format=self.image_format,
139
+ items=self.image_data)
140
+ )
141
+ )
142
+
143
+
144
+ class JsonResult(SapioToolResult):
145
+ """
146
+ A class representing JSON results from a Sapio tool.
147
+ """
148
+ json_data: list[Any]
149
+
150
+ def __init__(self, json_data: list[Any]):
151
+ """
152
+ :param json_data: The list of JSON data results. Each entry in the list represents a separate JSON object.
153
+ These entries must be able to be serialized to JSON using json.dumps().
154
+ """
155
+ self.json_data = json_data
156
+
157
+ def to_proto(self) -> StepOutputBatchPbo | list[FieldValueMapPbo]:
158
+ return StepOutputBatchPbo(
159
+ item_container=StepItemContainerPbo(
160
+ dataType=DataTypePbo.JSON,
161
+ json_container=StepJsonContainerPbo(items=[json.dumps(x) for x in self.json_data])
162
+ )
163
+ )
164
+
165
+
166
+ class TextResult(SapioToolResult):
167
+ """
168
+ A class representing text results from a Sapio tool.
169
+ """
170
+ text_data: list[str]
171
+
172
+ def __init__(self, text_data: list[str]):
173
+ """
174
+ :param text_data: The text data as a list of strings.
175
+ """
176
+ self.text_data = text_data
177
+
178
+ def to_proto(self) -> StepOutputBatchPbo | list[FieldValueMapPbo]:
179
+ return StepOutputBatchPbo(
180
+ item_container=StepItemContainerPbo(
181
+ dataType=DataTypePbo.TEXT,
182
+ text_container=StepTextContainerPbo(items=self.text_data)
183
+ )
184
+ )
185
+
186
+
187
+ class ToolServiceBase(ToolServiceServicer, ABC):
188
+ """
189
+ A base class for implementing a tool service. Subclasses should implement the register_tools method to register
190
+ their tools with the service.
191
+ """
192
+ def GetToolDetails(self, request: ToolDetailsRequestPbo, context: ServicerContext) -> ToolDetailsResponsePbo:
193
+ try:
194
+ # Get the tool details from the registered tools.
195
+ details: list[ToolDetailsPbo] = self.get_details()
196
+ return ToolDetailsResponsePbo(tool_framework_version=self.tool_version(), tool_details=details)
197
+ except Exception:
198
+ # TODO: This response doesn't even allow logs. What should we do if an exception occurs in this case?
199
+ return ToolDetailsResponsePbo()
200
+
201
+ def ProcessData(self, request: ProcessStepRequestPbo, context: ServicerContext) -> ProcessStepResponsePbo:
202
+ try:
203
+ # Convert the SapioConnectionInfo proto object to a SapioUser object.
204
+ user = self.create_user(request.sapio_user)
205
+ # Get the tool results from the registered tool matching the request and convert them to proto objects.
206
+ output_data: list[StepOutputBatchPbo] = []
207
+ new_records: list[FieldValueMapPbo] = []
208
+ # TODO: Make use of the success value after the response object has a field for it.
209
+ success, results, logs = self.run(user, request, context)
210
+ for result in results:
211
+ data: StepOutputBatchPbo | list[FieldValueMapPbo] = result.to_proto()
212
+ if isinstance(data, StepOutputBatchPbo):
213
+ output_data.append(data)
214
+ else:
215
+ new_records.extend(data)
216
+ # Return a ProcessStepResponse proto object containing the output data and new records to the caller.
217
+ return ProcessStepResponsePbo(output=output_data, log=logs, new_records=new_records)
218
+ except Exception:
219
+ # TODO: Return a False success result after the response object has a field for it.
220
+ return ProcessStepResponsePbo(log=[traceback.format_exc()])
221
+
222
+ @staticmethod
223
+ def create_user(info: SapioConnectionInfoPbo, timeout_seconds: int = 60) -> SapioUser:
224
+ """
225
+ Create a SapioUser object from the given SapioConnectionInfo proto object.
226
+
227
+ :param info: The SapioConnectionInfo proto object.
228
+ :param timeout_seconds: The request timeout for calls made from this user object.
229
+ """
230
+ # TODO: Have a customizable request timeout? Would need to be added to the request object.
231
+ # TODO: How should the RMI hosts and port be used in the connection info?
232
+ user = SapioUser(info.webservice_url, True, timeout_seconds, guid=info.app_guid)
233
+ if info.secret_type == SapioUserSecretTypePbo.SESSION_TOKEN:
234
+ user.api_token = info.secret
235
+ elif info.secret_type == SapioUserSecretTypePbo.PASSWORD:
236
+ # TODO: Will the secret be base64 encoded if it's a password? That's how basic auth is normally handled.
237
+ user.password = info.secret
238
+ else:
239
+ raise Exception(f"Unexpected secret type: {info.secret_type}")
240
+ return user
241
+
242
+ @staticmethod
243
+ def tool_version() -> int:
244
+ """
245
+ :return: The version of this tool.
246
+ """
247
+ return 1
248
+
249
+ def _get_tools(self) -> list[ToolBase]:
250
+ """
251
+ return: Get the tools registered with this service.
252
+ """
253
+ tools: list[ToolBase] = self.register_tools()
254
+ if not tools:
255
+ raise Exception("No tools registered with this service.")
256
+ return tools
257
+
258
+ def _get_tool(self, name: str) -> ToolBase:
259
+ """
260
+ Get a specific tool by its name.
261
+
262
+ :param name: The name of the tool to retrieve.
263
+ :return: The tool object corresponding to the given name.
264
+ """
265
+ tools: dict[str, ToolBase] = {x.name: x for x in self.register_tools()}
266
+ if not tools:
267
+ raise Exception("No tools registered with this service.")
268
+ if name not in tools:
269
+ raise Exception(f"Tool \"{name}\" not found in registered tools.")
270
+ return tools[name]
271
+
272
+ @abstractmethod
273
+ def register_tools(self) -> list[ToolBase]:
274
+ """
275
+ Register the tools with this service. Create and instantiate ToolBase subclasses to register them.
276
+
277
+ :return: A list of tools to register to this service.
278
+ """
279
+ pass
280
+
281
+ def get_details(self) -> list[ToolDetailsPbo]:
282
+ """
283
+ Get the details of the tool.
284
+
285
+ :return: A ToolDetailsResponse object containing the tool details.
286
+ """
287
+ tool_details: list[ToolDetailsPbo] = []
288
+ for tool in self._get_tools():
289
+ tool_details.append(tool.to_pbo())
290
+ return tool_details
291
+
292
+ def run(self, user: SapioUser, request: ProcessStepRequestPbo, context: ServicerContext) \
293
+ -> tuple[bool, list[SapioToolResult], list[str]]:
294
+ """
295
+ Execute a tool from this service.
296
+
297
+ :param user: A user object that can be used to initialize manager classes using DataMgmtServer to query the
298
+ system.
299
+ :param request: The request object containing the input data.
300
+ :param context: The gRPC context.
301
+ :return: Whether or not the tool succeeded, the results of the tool, and any logs generated by the tool.
302
+ """
303
+ tool = self._get_tool(request.tool_name)
304
+ try:
305
+ tool.setup(user, request, context)
306
+ results: list[SapioToolResult] = tool.run(user)
307
+ return True, results, tool.logs
308
+ except Exception:
309
+ tool.log_message(traceback.format_exc())
310
+ return False, [], tool.logs
311
+
312
+
313
+ class ToolBase(ABC):
314
+ """
315
+ A base class for implementing a tool.
316
+ """
317
+ name: str
318
+ description: str
319
+ data_type_name: str | None
320
+ inputs: list[ToolInputDetailsPbo]
321
+ outputs: list[ToolOutputDetailsPbo]
322
+ configs: list[VeloxFieldDefPbo]
323
+ logs: list[str]
324
+
325
+ user: SapioUser
326
+ request: ProcessStepRequestPbo
327
+ context: ServicerContext
328
+ verbose_logging: bool
329
+
330
+ def __init__(self, name: str, description: str, data_type_name: str | None = None):
331
+ """
332
+ :param name: The name of the tool.
333
+ :param description: A description of the tool.
334
+ :param data_type_name: The name of the output data type of this tool, if applicable. When this tool returns
335
+ FieldMapResult objects in its run method, this name will be used to set the data type of the output data.
336
+ """
337
+ self.name = name
338
+ self.description = description
339
+ self.data_type_name = data_type_name
340
+ self.inputs = []
341
+ self.outputs = []
342
+ self.configs = []
343
+ self.logs = []
344
+
345
+ def setup(self, user: SapioUser, request: ProcessStepRequestPbo, context: ServicerContext) -> None:
346
+ """
347
+ Setup the tool with the user, request, and context. This method can be overridden by subclasses to perform
348
+ additional setup.
349
+
350
+ :param user: A user object that can be used to initialize manager classes using DataMgmtServer to query the
351
+ system.
352
+ :param request: The request object containing the input data.
353
+ :param context: The gRPC context.
354
+ """
355
+ self.user = user
356
+ self.request = request
357
+ self.context = context
358
+ # TODO: Determine verbose logging from the request.
359
+ self.verbose_logging = False
360
+
361
+ def add_input(self, content_type: DataTypePbo, display_name: str, description: str, example: str | None = None,
362
+ validation: str | None = None, input_count: tuple[int, int] | None = None, is_paged: bool = False,
363
+ page_size: tuple[int, int] | None = None, max_request_bytes: int | None = None) -> None:
364
+ """
365
+ Add an input configuration to the tool. This determines how many inputs this tool will accept in the plan
366
+ manager, as well as what those inputs are. The IO number of the input will be set to the current number of
367
+ inputs. That is, the first time this is called, the IO number will be 0, the second time it is called, the IO
368
+ number will be 1, and so on.
369
+
370
+ :param content_type: The content type of the input.
371
+ :param display_name: The display name of the input.
372
+ :param description: The description of the input.
373
+ :param example: An optional example of the input.
374
+ :param validation: An optional validation string for the input.
375
+ :param input_count: A tuple of the minimum and maximum number of inputs allowed for this tool.
376
+ :param is_paged: If true, this input will be paged. If false, this input will not be paged.
377
+ :param page_size: A tuple of the minimum and maximum page size for this tool. The input must be paged in order
378
+ for this to have an effect.
379
+ :param max_request_bytes: The maximum request size in bytes for this tool.
380
+ """
381
+ self.inputs.append(ToolInputDetailsPbo(
382
+ base_config=ToolIoConfigBasePbo(
383
+ io_number=len(self.inputs),
384
+ content_type=ProtobufUtils.content_type_str(content_type),
385
+ display_name=display_name,
386
+ description=description,
387
+ example=example
388
+ ),
389
+ validation=validation,
390
+ min_input_count=input_count[0] if input_count else None,
391
+ max_input_count=input_count[1] if input_count else None,
392
+ paged=is_paged,
393
+ min_page_size=page_size[0] if page_size else None,
394
+ max_page_size=page_size[1] if page_size else None,
395
+ max_request_bytes=max_request_bytes,
396
+ ))
397
+
398
+ def add_output(self, content_type: DataTypePbo, display_name: str, description: str, example: str | None = None) -> None:
399
+ """
400
+ Add an output configuration to the tool. This determines how many inputs this tool will accept in the plan
401
+ manager, as well as what those inputs are. The IO number of the output will be set to the current number of
402
+ outputs. That is, the first time this is called, the IO number will be 0, the second time it is called, the IO
403
+ number will be 1, and so on.
404
+
405
+ :param content_type: The content type of the output.
406
+ :param display_name: The display name of the output.
407
+ :param description: The description of the output.
408
+ :param example: An example of the output.
409
+ """
410
+ self.outputs.append(ToolOutputDetailsPbo(
411
+ base_config=ToolIoConfigBasePbo(
412
+ io_number=len(self.outputs),
413
+ content_type=ProtobufUtils.content_type_str(content_type),
414
+ display_name=display_name,
415
+ description=description,
416
+ example=example
417
+ )))
418
+
419
+ def add_config_field(self, field: VeloxFieldDefPbo) -> None:
420
+ """
421
+ Add a configuration field to the tool. This field will be used to configure the tool in the plan manager.
422
+
423
+ :param field: The configuration field details.
424
+ """
425
+ self.configs.append(field)
426
+
427
+ def add_config_field_def(self, field: AbstractVeloxFieldDefinition) -> None:
428
+ """
429
+ Add a configuration field to the tool. This field will be used to configure the tool in the plan manager.
430
+
431
+ :param field: The configuration field details.
432
+ """
433
+ self.configs.append(ProtobufUtils.field_def_to_pbo(field))
434
+
435
+ def add_boolean_config_field(self, field_name: str, display_name: str, description: str, default_value: bool) \
436
+ -> None:
437
+ """
438
+ Add a boolean configuration field to the tool. This field will be used to configure the tool in the plan
439
+ manager.
440
+
441
+ :param field_name: The name of the field.
442
+ :param display_name: The display name of the field.
443
+ :param description: The description of the field.
444
+ :param default_value: The default value of the field.
445
+ """
446
+ self.configs.append(VeloxFieldDefPbo(
447
+ data_field_type=FieldTypePbo.BOOLEAN,
448
+ data_field_name=field_name,
449
+ display_name=display_name,
450
+ description=description,
451
+ required=True,
452
+ editable=True,
453
+ boolean_properties=BooleanPropertiesPbo(
454
+ default_value=default_value
455
+ )
456
+ ))
457
+
458
+ def add_double_config_field(self, field_name: str, display_name: str, description: str, default_value: float,
459
+ min_value: float = -10.**120, max_value: float = 10.**120, precision: int = 2) -> None:
460
+ """
461
+ Add a double configuration field to the tool. This field will be used to configure the tool in the plan
462
+ manager.
463
+
464
+ :param field_name: The name of the field.
465
+ :param display_name: The display name of the field.
466
+ :param description: The description of the field.
467
+ :param default_value: The default value of the field.
468
+ :param min_value: The minimum value of the field.
469
+ :param max_value: The maximum value of the field.
470
+ :param precision: The precision of the field.
471
+ """
472
+ self.configs.append(VeloxFieldDefPbo(
473
+ data_field_type=FieldTypePbo.DOUBLE,
474
+ data_field_name=field_name,
475
+ display_name=display_name,
476
+ description=description,
477
+ required=True,
478
+ editable=True,
479
+ double_properties=DoublePropertiesPbo(
480
+ default_value=default_value,
481
+ min_value=min_value,
482
+ max_value=max_value,
483
+ precision=precision
484
+ )
485
+ ))
486
+
487
+ def add_integer_config_field(self, field_name: str, display_name: str, description: str,
488
+ default_value: int, min_value: int = -2**31, max_value: int = 2**31-1) -> None:
489
+ """
490
+ Add an integer configuration field to the tool. This field will be used to configure the tool in the plan
491
+ manager.
492
+
493
+ :param field_name: The name of the field.
494
+ :param display_name: The display name of the field.
495
+ :param description: The description of the field.
496
+ :param default_value: The default value of the field.
497
+ :param min_value: The minimum value of the field.
498
+ :param max_value: The maximum value of the field.
499
+ """
500
+ self.configs.append(VeloxFieldDefPbo(
501
+ data_field_type=FieldTypePbo.INTEGER,
502
+ data_field_name=field_name,
503
+ display_name=display_name,
504
+ description=description,
505
+ required=True,
506
+ editable=True,
507
+ integer_properties=IntegerPropertiesPbo(
508
+ default_value=default_value,
509
+ min_value=min_value,
510
+ max_value=max_value
511
+ )
512
+ ))
513
+
514
+ def add_list_config_field(self, field_name: str, display_name: str, description: str, default_value: str,
515
+ allowed_values: list[str]) -> None:
516
+ """
517
+ Add a list configuration field to the tool. This field will be used to configure the tool in the plan
518
+ manager.
519
+
520
+ :param field_name: The name of the field.
521
+ :param display_name: The display name of the field.
522
+ :param description: The description of the field.
523
+ :param default_value: The default value of the field.
524
+ :param allowed_values: The list of allowed values for the field.
525
+ """
526
+ self.configs.append(VeloxFieldDefPbo(
527
+ data_field_type=FieldTypePbo.SELECTION,
528
+ data_field_name=field_name,
529
+ display_name=display_name,
530
+ description=description,
531
+ required=True,
532
+ editable=True,
533
+ selection_properties=SelectionPropertiesPbo(
534
+ default_value=default_value,
535
+ static_list_values=allowed_values,
536
+ )
537
+ ))
538
+
539
+ def add_multi_list_config_field(self, field_name: str, display_name: str, description: str,
540
+ default_value: list[str], allowed_values: list[str]) -> None:
541
+ """
542
+ Add a multi-select list configuration field to the tool. This field will be used to configure the tool in the
543
+ plan manager.
544
+
545
+ :param field_name: The name of the field.
546
+ :param display_name: The display name of the field.
547
+ :param description: The description of the field.
548
+ :param default_value: The default value of the field.
549
+ :param allowed_values: The list of allowed values for the field.
550
+ """
551
+ self.configs.append(VeloxFieldDefPbo(
552
+ data_field_type=FieldTypePbo.SELECTION,
553
+ data_field_name=field_name,
554
+ display_name=display_name,
555
+ description=description,
556
+ required=True,
557
+ editable=True,
558
+ selection_properties=SelectionPropertiesPbo(
559
+ default_value=",".join(default_value),
560
+ static_list_values=allowed_values,
561
+ multi_select=True,
562
+ )
563
+ ))
564
+
565
+ def to_pbo(self) -> ToolDetailsPbo:
566
+ """
567
+ :return: The ToolDetailsPbo proto object representing this tool.
568
+ """
569
+ return ToolDetailsPbo(
570
+ name=self.name,
571
+ description=self.description,
572
+ input_configs=self.inputs,
573
+ output_configs=self.outputs,
574
+ output_data_type_name=self.data_type_name,
575
+ config_fields=self.configs
576
+ )
577
+
578
+ @abstractmethod
579
+ def run(self, user: SapioUser) -> list[SapioToolResult]:
580
+ """
581
+ Execute this tool.
582
+
583
+ The request inputs can be accessed using the self.get_input_*() methods.
584
+ The request settings can be accessed using the self.get_config_fields() method.
585
+ The request itself can be accessed using self.request.
586
+
587
+ :param user: A user object that can be used to initialize manager classes using DataMgmtServer to query the
588
+ system.
589
+ :return: A SapioToolResults object containing the response data. Each result in the list corresponds to a
590
+ separate output from the tool. Field map results do not appear as tool output in the plan manager, instead
591
+ appearing as records related to the plan step during the run.
592
+ """
593
+ pass
594
+
595
+ def log_message(self, message: str, verbose: bool = False) -> None:
596
+ """
597
+ Log a message for this tool. This message will be included in the logs returned to the caller.
598
+
599
+ :param message: The message to log.
600
+ :param verbose: If true, the message will only be logged if verbose logging is enabled. If false, the message
601
+ will be logged regardless of the verbose logging setting.
602
+ """
603
+ if not verbose or self.verbose_logging:
604
+ self.logs.append(f"{self.name}: {message}")
605
+
606
+ def get_input_binary(self, index: int = 0) -> list[bytes]:
607
+ """
608
+ Get the binary data from the request object.
609
+
610
+ :param index: The index of the input to parse. Defaults to 0. Used for tools that accept multiple inputs.
611
+ :return: The binary data from the request object.
612
+ """
613
+ return list(self.request.input[index].item_container.binary_container.items)
614
+
615
+ def get_input_csv(self, index: int = 0) -> tuple[list[str], list[dict[str, str]]]:
616
+ """
617
+ Parse the CSV data from the request object.
618
+
619
+ :param index: The index of the input to parse. Defaults to 0. Used for tools that accept multiple inputs.
620
+ :return: A tuple containing the header row and the data rows. The header row is a list of strings representing
621
+ the column names, and the data rows are a list of dictionaries where each dictionary represents a row in the
622
+ CSV with the column names as keys and the corresponding values as strings.
623
+ """
624
+ input_data: Sequence[StepInputBatchPbo] = self.request.input
625
+ ret_val: list[dict[str, str]] = []
626
+ headers: Iterable[str] = input_data[index].item_container.csv_container.header.cells
627
+ for row in input_data[index].item_container.csv_container.items:
628
+ row_dict: dict[str, str] = {}
629
+ for header, value in zip(headers, row.cells):
630
+ row_dict[header] = value
631
+ ret_val.append(row_dict)
632
+ return list(headers), ret_val
633
+
634
+ def get_input_images(self, index: int = 0) -> tuple[str, list[bytes]]:
635
+ """
636
+ Parse the image data from the request object.
637
+
638
+ :param index: The index of the input to parse. Defaults to 0. Used for tools that accept multiple inputs.
639
+ :return: A tuple containing the image format and the image data. The image format is a string representing the
640
+ format of the image (e.g., PNG, JPEG), and the image data is a list of bytes representing the image data.
641
+ Each entry in the list represents a separate image.
642
+ """
643
+ image_data: StepImageContainerPbo = self.request.input[index].item_container.image_container
644
+ return image_data.image_format, list(image_data.items)
645
+
646
+ def get_input_json(self, index: int = 0) -> list[list[Any]] | list[dict[str, Any]]:
647
+ """
648
+ Parse the JSON data from the request object.
649
+
650
+ :param index: The index of the input to parse. Defaults to 0. Used for tools that accept multiple inputs.
651
+ :return: A list of parsed JSON objects. Each entry in the list represents a separate JSON entry from the input.
652
+ Depending on this tool, this may be a list of dictionaries or a list of lists.
653
+ """
654
+ return [json.loads(x) for x in self.request.input[index].item_container.json_container.items]
655
+
656
+ def get_input_text(self, index: int = 0) -> list[str]:
657
+ """
658
+ Parse the text data from the request object.
659
+
660
+ :param index: The index of the input to parse. Defaults to 0. Used for tools that accept multiple inputs.
661
+ :return: A list of text data as strings.
662
+ """
663
+ return list(self.request.input[index].item_container.text_container.items)
664
+
665
+ def get_config_defs(self) -> dict[str, VeloxFieldDefPbo]:
666
+ """
667
+ Get the config field definitions for this tool.
668
+
669
+ :return: A dictionary of field definitions, where the keys are the field names and the values are the
670
+ VeloxFieldDefPbo objects representing the field definitions.
671
+ """
672
+ field_defs: dict[str, VeloxFieldDefPbo] = {}
673
+ for field_def in self.to_pbo().config_fields:
674
+ field_defs[field_def.data_field_name] = field_def
675
+ return field_defs
676
+
677
+ def get_config_fields(self) -> dict[str, FieldValue]:
678
+ """
679
+ Get the configuration fields from the request object.
680
+
681
+ :return: A dictionary of configuration field names and their values.
682
+ """
683
+ config_fields: dict[str, Any] = {}
684
+ field_defs: dict[str, VeloxFieldDefPbo] = self.get_config_defs()
685
+ for field_name, value in self.request.config_field_values.items():
686
+ if field_name not in field_defs:
687
+ continue
688
+ config_fields[field_name] = ProtobufUtils.field_pbo_to_value(field_defs[field_name], value)
689
+ return config_fields
690
+
691
+ @staticmethod
692
+ def read_from_json(json_data: list[dict[str, Any]], key: str) -> list[Any]:
693
+ """
694
+ From a list of dictionaries, return a list of values for the given key from each dictionary. Skips null values.
695
+
696
+ :param json_data: The JSON data to read from.
697
+ :param key: The key to read the values from.
698
+ :return: A list of values corresponding to the given key in the JSON data.
699
+ """
700
+ ret_val: list[Any] = []
701
+ for entry in json_data:
702
+ if key in entry:
703
+ value = entry[key]
704
+ if isinstance(value, list):
705
+ ret_val.extend(value)
706
+ elif value is not None:
707
+ ret_val.append(value)
708
+ return ret_val