sapiopycommons 2025.10.10a779__py3-none-any.whl → 2025.10.15a782__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.

@@ -908,7 +908,7 @@ class AgentBase(ABC):
908
908
  if len(matching_creds) > 1:
909
909
  raise ValueError(f"Multiple credentials found for category '{category}' and host '{host}'.")
910
910
 
911
- return ExternalCredentials(matching_creds[0])
911
+ return ExternalCredentials.from_pbo(matching_creds[0])
912
912
 
913
913
  def call_subprocess(self,
914
914
  args: str | bytes | PathLike[str] | PathLike[bytes] | Sequence[str | bytes | PathLike[str] | PathLike[bytes]],
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from sapiopycommons.ai.protoapi.externalcredentials.external_credentials_pb2 import ExternalCredentialsPbo
2
4
 
3
5
 
@@ -5,26 +7,97 @@ class ExternalCredentials:
5
7
  """
6
8
  A class representing external credentials.
7
9
  """
8
- id: str
9
- display_name: str
10
- description: str
11
- category: str
12
- url: str
13
- username: str
14
- password: str
15
- token: str
16
- custom_fields: dict[str, str]
17
-
18
- def __init__(self, pbo: ExternalCredentialsPbo):
19
- self.id = pbo.id
20
- self.display_name = pbo.display_name
21
- self.description = pbo.description
22
- self.category = pbo.category
23
- self.url = pbo.url
24
- self.username = pbo.username
25
- self.password = pbo.password
26
- self.token = pbo.token
27
- self.custom_fields = dict(pbo.custom_field)
10
+ _identifier: str
11
+ _display_name: str
12
+ _description: str
13
+ _category: str
14
+ _url: str
15
+ _username: str
16
+ _password: str
17
+ _token: str
18
+ _custom_fields: dict[str, str]
19
+
20
+ def __init__(self, url: str, category: str, identifier: str = "", display_name: str = "", description: str = "",
21
+ username: str = "", password: str = "", token: str = "", custom_fields: dict[str, str] | None = None):
22
+ """
23
+ :param url: The URL that the credentials are for.
24
+ :param category: The category of the credentials. This can be used to search for the credentials using the
25
+ AgentBase.get_credentials function.
26
+ :param identifier: The unique identifier for the credentials.
27
+ :param display_name: The display name for the credentials.
28
+ :param description: A description of the credentials.
29
+ :param username: The username for the credentials.
30
+ :param password: The password for the credentials.
31
+ :param token: The token for the credentials.
32
+ :param custom_fields: A dictionary of custom fields associated with the credentials.
33
+ """
34
+ self._identifier = identifier
35
+ self._display_name = display_name
36
+ self._description = description
37
+ self._category = category
38
+ self._url = url
39
+ self._username = username
40
+ self._password = password
41
+ self._token = token
42
+ self._custom_fields = custom_fields or {}
43
+
44
+ @staticmethod
45
+ def from_pbo(pbo: ExternalCredentialsPbo) -> ExternalCredentials:
46
+ """
47
+ Create an ExternalCredentials instance from a protobuf object.
48
+
49
+ :param pbo: An ExternalCredentialsPbo object.
50
+ :return: An ExternalCredentials instance.
51
+ """
52
+ creds = ExternalCredentials(pbo.url, pbo.category)
53
+ creds._identifier = pbo.id
54
+ creds._display_name = pbo.display_name
55
+ creds._description = pbo.description
56
+ creds._username = pbo.username
57
+ creds._password = pbo.password
58
+ creds._token = pbo.token
59
+ creds._custom_fields = dict(pbo.custom_field)
60
+ return creds
61
+
62
+ @property
63
+ def identifier(self) -> str:
64
+ """The unique identifier for the credentials."""
65
+ return self._identifier
66
+
67
+ @property
68
+ def display_name(self) -> str:
69
+ """The display name for the credentials."""
70
+ return self._display_name
71
+
72
+ @property
73
+ def description(self) -> str:
74
+ """A description of the credentials."""
75
+ return self._description
76
+
77
+ @property
78
+ def category(self) -> str:
79
+ """The category of the credentials."""
80
+ return self._category
81
+
82
+ @property
83
+ def url(self) -> str:
84
+ """The URL that the credentials are for."""
85
+ return self._url
86
+
87
+ @property
88
+ def username(self) -> str:
89
+ """The username for the credentials."""
90
+ return self._username
91
+
92
+ @property
93
+ def password(self) -> str:
94
+ """The password for the credentials."""
95
+ return self._password
96
+
97
+ @property
98
+ def token(self) -> str:
99
+ """The token for the credentials."""
100
+ return self._token
28
101
 
29
102
  def get_custom_field(self, key: str, default: str = None) -> str | None:
30
103
  """
@@ -34,4 +107,22 @@ class ExternalCredentials:
34
107
  :param default: The value to return if the key does not exist.
35
108
  :return: The value of the custom field, or None if the key does not exist.
36
109
  """
37
- return self.custom_fields.get(key, default)
110
+ return self._custom_fields.get(key, default)
111
+
112
+ def to_pbo(self) -> ExternalCredentialsPbo:
113
+ """
114
+ Convert the ExternalCredentials instance to a protobuf object.
115
+
116
+ :return: An ExternalCredentialsPbo object.
117
+ """
118
+ return ExternalCredentialsPbo(
119
+ id=self._identifier,
120
+ display_name=self._display_name,
121
+ description=self._description,
122
+ category=self._category,
123
+ url=self._url,
124
+ username=self._username,
125
+ password=self._password,
126
+ token=self._token,
127
+ custom_field=self._custom_fields
128
+ )
@@ -1,11 +1,14 @@
1
1
  import base64
2
2
  import json
3
+ import os
3
4
  from enum import Enum
4
5
  from typing import Any
5
6
 
6
7
  import grpc
7
8
  from sapiopylib.rest.User import SapioUser
8
9
 
10
+ from sapiopycommons.ai.external_credentials import ExternalCredentials
11
+ from sapiopycommons.ai.protoapi.externalcredentials.external_credentials_pb2 import ExternalCredentialsPbo
9
12
  from sapiopycommons.ai.protoapi.fielddefinitions.fields_pb2 import FieldValuePbo
10
13
  from sapiopycommons.ai.protoapi.plan.converter.converter_pb2 import ConverterDetailsRequestPbo, \
11
14
  ConverterDetailsResponsePbo, ConvertResponsePbo, ConvertRequestPbo
@@ -20,6 +23,7 @@ from sapiopycommons.ai.protoapi.plan.tool.tool_pb2_grpc import ToolServiceStub
20
23
  from sapiopycommons.ai.protoapi.session.sapio_conn_info_pb2 import SapioConnectionInfoPbo, SapioUserSecretTypePbo
21
24
  from sapiopycommons.ai.protobuf_utils import ProtobufUtils
22
25
  from sapiopycommons.general.aliases import FieldValue
26
+ from sapiopycommons.general.time_util import TimeUtil
23
27
 
24
28
 
25
29
  class ContainerType(Enum):
@@ -63,39 +67,43 @@ class AgentOutput:
63
67
  self.new_records = []
64
68
  self.logs = []
65
69
 
66
- def save_outputs(self, path: str = "test_outputs", file_extensions: list[str] | None = None) -> None:
70
+ def save_outputs(self, path: str = "test_outputs", subfolder: str | None = None,
71
+ file_extensions: list[str] | None = None) -> None:
67
72
  """
68
- Save all outputs to files in the specified directory. The files will be named according to the agent name
69
- and the output type.
73
+ Save all outputs to files in the specified output directory.
70
74
 
71
75
  :param path: The directory to save the output files to.
76
+ :param subfolder: An optional subfolder within the path to save the output files to. Useful for when you are
77
+ calling the same agent multiple times for separate test cases.
72
78
  :param file_extensions: A list of file extensions to use for binary output files. The length of this list
73
79
  should match the number of binary outputs.
74
80
  """
75
81
  if not self:
76
82
  return
77
- import os
78
- os.makedirs(path, exist_ok=True)
79
- if self.binary_output and (not file_extensions or len(file_extensions) != len(self.binary_output)):
83
+ output_path: str = os.path.join(path, self.agent_name)
84
+ if subfolder:
85
+ output_path = os.path.join(output_path, subfolder)
86
+ os.makedirs(output_path, exist_ok=True)
87
+ if self.binary_output and (file_extensions is None or len(file_extensions) != len(self.binary_output)):
80
88
  raise ValueError("File extensions must be provided for each binary output.")
81
89
  for i, output in enumerate(self.binary_output):
82
90
  ext: str = "." + file_extensions[i].lstrip(".")
83
91
  for j, entry in enumerate(output):
84
- with open(os.path.join(path, self.agent_name, f"binary_output_{i}_{j}{ext}"), "wb") as f:
92
+ with open(os.path.join(output_path, f"binary_output_{i}_{j}{ext}"), "wb") as f:
85
93
  f.write(entry)
86
94
  for i, output in enumerate(self.csv_output):
87
- with open(os.path.join(path, self.agent_name, f"csv_output_{i}.csv"), "w", encoding="utf-8") as f:
95
+ with open(os.path.join(output_path, f"csv_output_{i}.csv"), "w", encoding="utf-8") as f:
88
96
  headers = output[0].keys()
89
97
  f.write(",".join(headers) + "\n")
90
98
  for row in output:
91
99
  f.write(",".join(f'"{str(row[h])}"' for h in headers) + "\n")
92
100
  for i, output in enumerate(self.json_output):
93
101
  for j, entry in enumerate(output):
94
- with open(os.path.join(path, self.agent_name, f"json_output_{i}_{j}.json"), "w", encoding="utf-8") as f:
102
+ with open(os.path.join(output_path, f"json_output_{i}_{j}.json"), "w", encoding="utf-8") as f:
95
103
  json.dump(entry, f, indent=2)
96
104
  for i, output in enumerate(self.text_output):
97
105
  for j, entry in enumerate(output):
98
- with open(os.path.join(path, self.agent_name, f"text_output_{i}_{j}.txt"), "w", encoding="utf-8") as f:
106
+ with open(os.path.join(output_path, f"text_output_{i}_{j}.txt"), "w", encoding="utf-8") as f:
99
107
  f.write(entry)
100
108
 
101
109
  def __bool__(self):
@@ -163,6 +171,7 @@ class TestClient:
163
171
  connection: SapioConnectionInfoPbo
164
172
  _request_inputs: list[StepItemContainerPbo]
165
173
  _config_fields: dict[str, FieldValuePbo]
174
+ _credentials: list[ExternalCredentialsPbo]
166
175
 
167
176
  def __init__(self, grpc_server_url: str, message_mb_size: int = 1024, user: SapioUser | None = None,
168
177
  options: list[tuple[str, Any]] | None = None):
@@ -183,6 +192,7 @@ class TestClient:
183
192
  self._create_connection(user)
184
193
  self._request_inputs = []
185
194
  self._config_fields = {}
195
+ self._credentials = []
186
196
 
187
197
  def _create_connection(self, user: SapioUser | None = None):
188
198
  """
@@ -267,10 +277,28 @@ class TestClient:
267
277
  """
268
278
  self._config_fields.clear()
269
279
 
280
+ def add_credentials(self, credentials: list[ExternalCredentials]) -> None:
281
+ """
282
+ Add external credentials to the connection info for the next request.
283
+
284
+ :param credentials: A list of ExternalCredentialsPbo objects to add to the connection info.
285
+ """
286
+ for cred in credentials:
287
+ self._credentials.append(cred.to_pbo())
288
+
289
+ def clear_credentials(self) -> None:
290
+ """
291
+ Clear all external credentials that have been added to the next request.
292
+ This is useful if you want to start a new request without the previous credentials.
293
+ """
294
+ self._credentials.clear()
295
+
270
296
  def clear_request(self) -> None:
271
297
  """
272
298
  Clear all inputs and configuration fields that have been added to the next request.
273
299
  This is useful if you want to start a new request without the previous inputs and configurations.
300
+
301
+ Credentials are not cleared, as they may be reused across multiple requests.
274
302
  """
275
303
  self.clear_inputs()
276
304
  self.clear_configs()
@@ -314,9 +342,11 @@ class TestClient:
314
342
  :param is_dry_run: If True, the agent will not be executed, but the request will be validated.
315
343
  :return: An AgentOutput object containing the results of the agent service call.
316
344
  """
345
+ print(f"Calling agent \"{agent_name}\"...")
317
346
  with grpc.insecure_channel(self.grpc_server_url, options=self.options) as channel:
318
347
  stub = ToolServiceStub(channel)
319
348
 
349
+ start = TimeUtil.now_in_millis()
320
350
  response: ProcessStepResponsePbo = stub.ProcessData(
321
351
  ProcessStepRequestPbo(
322
352
  sapio_user=self.connection,
@@ -324,12 +354,15 @@ class TestClient:
324
354
  config_field_values=self._config_fields,
325
355
  dry_run=is_dry_run,
326
356
  verbose_logging=is_verbose,
357
+ external_credential=self._credentials,
327
358
  input=[
328
359
  StepInputBatchPbo(is_partial=False, item_container=item)
329
360
  for item in self._request_inputs
330
361
  ]
331
362
  )
332
363
  )
364
+ end = TimeUtil.now_in_millis()
365
+ print(f"Agent call completed in {(end - start) / 1000.:.3f} seconds")
333
366
 
334
367
  results = AgentOutput(agent_name)
335
368
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2025.10.10a779
3
+ Version: 2025.10.15a782
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,12 +1,12 @@
1
1
  sapiopycommons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  sapiopycommons/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- sapiopycommons/ai/agent_service_base.py,sha256=UF5mSCXMxX0qUtEUWRtQ6_690aOo6pCRB89dyj8Amug,56292
3
+ sapiopycommons/ai/agent_service_base.py,sha256=LVZg_4Kw5WTcIB7ru5hEqJoMWQr2eTDYjMrX8pzpn9I,56301
4
4
  sapiopycommons/ai/converter_service_base.py,sha256=HiUXmwqv1STgyQeF9_eTFXzjIFXp5-NJ7sEhMpV3aAU,6351
5
- sapiopycommons/ai/external_credentials.py,sha256=40AI7VtHf6PzvwJLR_mZemUCrfAUvC--tGH-npaDIgo,1163
5
+ sapiopycommons/ai/external_credentials.py,sha256=ki_xIH4J843b_sSwEa8YHr8vW9erVv-jowZJXSgPQs8,4347
6
6
  sapiopycommons/ai/protobuf_utils.py,sha256=cBjbxoFAwU02kNUxEce95WnMU2CMuDD-qFaeWgvQJMQ,24599
7
7
  sapiopycommons/ai/request_validation.py,sha256=TD2EUi_G5cy1OOVK1AmY2SQc3PEoAKGWs2pT8Qnp2Oo,25092
8
8
  sapiopycommons/ai/server.py,sha256=gutSskn_Fenq1uz0DDMvjx4QVFiKt2WVEP3-01a69eU,6384
9
- sapiopycommons/ai/test_client.py,sha256=jHIKaBy2jta2UCcci6z0dvX_BZZuiV_0z2HPr9dEI_s,18897
9
+ sapiopycommons/ai/test_client.py,sha256=IRZ-8prhg7XMDmN9aC1MQr5mSkMgfT37aLsOy-VB-MU,20495
10
10
  sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2.py,sha256=mEonoj6Iq-AyvO4m3YsPYu85aZfD1E0a0cL8B9yPfEo,2481
11
11
  sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2.pyi,sha256=sfExq8fFwIwFxCpV50ytdxW5ePNBjJBr_80_trq_JQw,1658
12
12
  sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2_grpc.py,sha256=TNS1CB_QGBSa1YU9sYR_hF-pmBwv2GpxjaNQoM_r9iU,948
@@ -106,7 +106,7 @@ sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
106
106
  sapiopycommons/webhook/webhook_context.py,sha256=D793uLsb1691SalaPnBUk3rOSxn_hYLhdvkaIxjNXss,1909
107
107
  sapiopycommons/webhook/webhook_handlers.py,sha256=7o_wXOruhT9auNh8OfhJAh4WhhiPKij67FMBSpGPICc,39939
108
108
  sapiopycommons/webhook/webservice_handlers.py,sha256=cvW6Mk_110BzYqkbk63Kg7jWrltBCDALOlkJRu8h4VQ,14300
109
- sapiopycommons-2025.10.10a779.dist-info/METADATA,sha256=5rW00M6gy8crugEHSZZCOCNdoe-Bi8gbVBwfOqoNk0U,3143
110
- sapiopycommons-2025.10.10a779.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
111
- sapiopycommons-2025.10.10a779.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
112
- sapiopycommons-2025.10.10a779.dist-info/RECORD,,
109
+ sapiopycommons-2025.10.15a782.dist-info/METADATA,sha256=DIeUTx52EZpA1ZIecBFRA4_LsTMKBwOZLzB6i2iBgxs,3143
110
+ sapiopycommons-2025.10.15a782.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
111
+ sapiopycommons-2025.10.15a782.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
112
+ sapiopycommons-2025.10.15a782.dist-info/RECORD,,