Skip to content

Commit cb4a88e

Browse files
feat: extend invoke process with attachments support (#1221)
1 parent db4cc9a commit cb4a88e

8 files changed

Lines changed: 86 additions & 37 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.6.17"
3+
version = "2.6.18"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/platform/attachments/attachments.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import uuid
44
from dataclasses import dataclass
55
from enum import Enum
6-
from typing import Optional
6+
from typing import Any, Optional
77

88
from pydantic import BaseModel, Field
99

@@ -18,10 +18,10 @@ class AttachmentMode(str, Enum):
1818
class Attachment(BaseModel):
1919
"""Model representing an attachment. Id 'None' is used for uploads."""
2020

21-
id: Optional[uuid.UUID] = Field(None, alias="ID")
21+
id: uuid.UUID = Field(..., alias="ID")
2222
full_name: str = Field(..., alias="FullName")
2323
mime_type: str = Field(..., alias="MimeType")
24-
metadata: Optional[dict[str, str]] = Field(None, alias="Metadata")
24+
metadata: Optional[dict[str, Any]] = Field(None, alias="Metadata")
2525
model_config = {
2626
"title": "UiPathAttachment",
2727
"validate_by_name": True,

src/uipath/platform/common/interrupt_models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
)
1010

1111
from ..action_center.tasks import Task, TaskRecipient
12+
from ..attachments import Attachment
1213
from ..context_grounding import (
1314
BatchTransformCreationResponse,
1415
BatchTransformOutputColumn,
@@ -33,6 +34,7 @@ class InvokeProcess(BaseModel):
3334
process_folder_path: str | None = None
3435
process_folder_key: str | None = None
3536
input_arguments: dict[str, Any] | None
37+
attachments: list[Attachment] | None = None
3638

3739

3840
class WaitJob(BaseModel):

src/uipath/platform/orchestrator/_processes_service.py

Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ..._utils import Endpoint, RequestSpec, header_folder, resource_override
77
from ..._utils.constants import ENV_JOB_KEY, HEADER_JOB_KEY
88
from ...tracing import traced
9+
from ..attachments import Attachment
910
from ..common import BaseService, FolderContext, UiPathApiConfig, UiPathExecutionContext
1011
from ._attachments_service import AttachmentsService
1112
from .job import Job
@@ -19,6 +20,8 @@ class ProcessesService(FolderContext, BaseService):
1920
specific business tasks.
2021
"""
2122

23+
_INPUT_ARGUMENTS_SIZE_LIMIT = 10000
24+
2225
def __init__(
2326
self,
2427
config: UiPathApiConfig,
@@ -37,6 +40,7 @@ def invoke(
3740
*,
3841
folder_key: Optional[str] = None,
3942
folder_path: Optional[str] = None,
43+
attachments: Optional[list[Attachment]] = None,
4044
) -> Job:
4145
"""Start execution of a process by its name.
4246
@@ -45,6 +49,7 @@ def invoke(
4549
Args:
4650
name (str): The name of the process to execute.
4751
input_arguments (Optional[Dict[str, Any]]): The input arguments to pass to the process.
52+
attachments (Optional[list]): List of Attachment objects to pass to the process.
4853
folder_key (Optional[str]): The key of the folder to execute the process in. Override the default one set in the SDK config.
4954
folder_path (Optional[str]): The path of the folder to execute the process in. Override the default one set in the SDK config.
5055
@@ -72,9 +77,11 @@ def invoke(
7277
"""
7378
input_data = self._handle_input_arguments(
7479
input_arguments=input_arguments,
80+
attachments=attachments,
7581
folder_key=folder_key,
7682
folder_path=folder_path,
7783
)
84+
7885
spec = self._invoke_spec(
7986
name,
8087
input_data=input_data,
@@ -101,6 +108,7 @@ async def invoke_async(
101108
*,
102109
folder_key: Optional[str] = None,
103110
folder_path: Optional[str] = None,
111+
attachments: Optional[list[Attachment]] = None,
104112
) -> Job:
105113
"""Asynchronously start execution of a process by its name.
106114
@@ -109,6 +117,7 @@ async def invoke_async(
109117
Args:
110118
name (str): The name of the process to execute.
111119
input_arguments (Optional[Dict[str, Any]]): The input arguments to pass to the process.
120+
attachments (Optional[list]): List of Attachment objects to pass to the process.
112121
folder_key (Optional[str]): The key of the folder to execute the process in. Override the default one set in the SDK config.
113122
folder_path (Optional[str]): The path of the folder to execute the process in. Override the default one set in the SDK config.
114123
@@ -132,6 +141,7 @@ async def main():
132141
"""
133142
input_data = await self._handle_input_arguments_async(
134143
input_arguments=input_arguments,
144+
attachments=attachments,
135145
folder_key=folder_key,
136146
folder_path=folder_path,
137147
)
@@ -157,71 +167,104 @@ async def main():
157167
def custom_headers(self) -> Dict[str, str]:
158168
return self.folder_headers
159169

170+
@staticmethod
171+
def _prepare_link_attachments(
172+
attachments: Optional[list[Attachment]],
173+
) -> Optional[list[Dict[str, str]]]:
174+
"""Format attachments for process invocation payload."""
175+
if not attachments:
176+
return None
177+
178+
link_attachments = [
179+
{"attachmentId": str(att.id)} for att in attachments if att.id is not None
180+
]
181+
return link_attachments if link_attachments else None
182+
160183
def _handle_input_arguments(
161184
self,
162185
input_arguments: Optional[Dict[str, Any]] = None,
186+
attachments: Optional[list[Attachment]] = None,
163187
*,
164188
folder_key: Optional[str] = None,
165189
folder_path: Optional[str] = None,
166-
) -> Dict[str, str]:
167-
"""Handle input arguments, storing as attachment if they exceed size limit.
190+
) -> Dict[str, Any]:
191+
"""Handle input arguments and attachments, storing as attachment if they exceed size limit.
168192
169193
Args:
170194
input_arguments: The input arguments to process
195+
attachments: List of Attachment objects to pass to the process
171196
folder_key: The folder key for attachment storage
172197
folder_path: The folder path for attachment storage
173198
174199
Returns:
175-
Dict containing either "InputArguments" or "InputFile" key
200+
Dict containing either "InputArguments" or "InputFile" key, and optionally "Attachments"
176201
"""
202+
result: Dict[str, Any] = {}
203+
204+
# handle input arguments
177205
if not input_arguments:
178-
return {"InputArguments": json.dumps({})}
179-
180-
# If payload exceeds limit, store as attachment
181-
payload_json = json.dumps(input_arguments)
182-
if len(payload_json) > 10000: # 10k char limit
183-
attachment_id = self._attachments_service.upload(
184-
name=f"{uuid.uuid4()}.json",
185-
content=payload_json,
186-
folder_key=folder_key,
187-
folder_path=folder_path,
188-
)
189-
return {"InputFile": str(attachment_id)}
206+
result["InputArguments"] = json.dumps({})
190207
else:
191-
return {"InputArguments": payload_json}
208+
# If payload exceeds limit, store as attachment
209+
payload_json = json.dumps(input_arguments)
210+
if len(payload_json) > self._INPUT_ARGUMENTS_SIZE_LIMIT:
211+
attachment_id = self._attachments_service.upload(
212+
name=f"{uuid.uuid4()}.json",
213+
content=payload_json,
214+
folder_key=folder_key,
215+
folder_path=folder_path,
216+
)
217+
result["InputFile"] = str(attachment_id)
218+
else:
219+
result["InputArguments"] = payload_json
220+
221+
link_attachments = self._prepare_link_attachments(attachments)
222+
if link_attachments:
223+
result["Attachments"] = link_attachments
224+
225+
return result
192226

193227
async def _handle_input_arguments_async(
194228
self,
195229
input_arguments: Optional[Dict[str, Any]] = None,
230+
attachments: Optional[list[Attachment]] = None,
196231
*,
197232
folder_key: Optional[str] = None,
198233
folder_path: Optional[str] = None,
199-
) -> Dict[str, str]:
200-
"""Handle input arguments, storing as attachment if they exceed size limit.
234+
) -> Dict[str, Any]:
235+
"""Handle input arguments and attachments, storing as attachment if they exceed size limit.
201236
202237
Args:
203238
input_arguments: The input arguments to process
239+
attachments: List of Attachment objects to pass to the process
204240
folder_key: The folder key for attachment storage
205241
folder_path: The folder path for attachment storage
206242
207243
Returns:
208-
Dict containing either "InputArguments" or "InputFile" key
244+
Dict containing either "InputArguments" or "InputFile" key, and optionally "Attachments"
209245
"""
246+
result: Dict[str, Any] = {}
247+
210248
if not input_arguments:
211-
return {"InputArguments": json.dumps({})}
212-
213-
# If payload exceeds limit, store as attachment
214-
payload_json = json.dumps(input_arguments)
215-
if len(payload_json) > 10000: # 10k char limit
216-
attachment_id = await self._attachments_service.upload_async(
217-
name=f"{uuid.uuid4()}.json",
218-
content=payload_json,
219-
folder_key=folder_key,
220-
folder_path=folder_path,
221-
)
222-
return {"InputFile": str(attachment_id)}
249+
result["InputArguments"] = json.dumps({})
223250
else:
224-
return {"InputArguments": payload_json}
251+
payload_json = json.dumps(input_arguments)
252+
if len(payload_json) > self._INPUT_ARGUMENTS_SIZE_LIMIT:
253+
attachment_id = await self._attachments_service.upload_async(
254+
name=f"{uuid.uuid4()}.json",
255+
content=payload_json,
256+
folder_key=folder_key,
257+
folder_path=folder_path,
258+
)
259+
result["InputFile"] = str(attachment_id)
260+
else:
261+
result["InputArguments"] = payload_json
262+
263+
formatted_attachments = self._prepare_link_attachments(attachments)
264+
if formatted_attachments:
265+
result["Attachments"] = formatted_attachments
266+
267+
return result
225268

226269
def _invoke_spec(
227270
self,

src/uipath/platform/resume_triggers/_protocol.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,7 @@ async def _handle_job_trigger(
774774
job = await uipath.processes.invoke_async(
775775
name=value.name,
776776
input_arguments=value.input_arguments,
777+
attachments=value.attachments,
777778
folder_path=value.process_folder_path,
778779
folder_key=value.process_folder_key,
779780
)

tests/cli/test_hitl.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,7 @@ async def test_create_resume_trigger_invoke_process(
891891
mock_process_invoke_async.assert_called_once_with(
892892
name=invoke_process.name,
893893
input_arguments=invoke_process.input_arguments,
894+
attachments=None,
894895
folder_path=invoke_process.process_folder_path,
895896
folder_key=None,
896897
)

tests/sdk/services/test_attachments_service.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,7 @@ def test_open_write_mode(
993993
file_name = "test_write_file.txt"
994994
file_content = b"Content to write"
995995
attachment = Attachment( # type: ignore[call-arg]
996+
ID=uuid.uuid4(),
996997
FullName=file_name,
997998
MimeType="text/plain",
998999
)
@@ -1144,6 +1145,7 @@ async def test_open_async_write_mode(
11441145
file_name = "test_write_file_async.txt"
11451146
file_content = b"Content to write async"
11461147
attachment = Attachment( # type: ignore[call-arg]
1148+
ID=uuid.uuid4(),
11471149
FullName=file_name,
11481150
MimeType="text/plain",
11491151
)

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)