Skip to content

Commit 951ab80

Browse files
committed
feat(sdk): add job-attachments ease-of-use methods
1 parent ea0476c commit 951ab80

File tree

4 files changed

+466
-0
lines changed

4 files changed

+466
-0
lines changed

src/uipath/_services/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .connections_service import ConnectionsService
77
from .context_grounding_service import ContextGroundingService
88
from .folder_service import FolderService
9+
from .job_attachments_service import JobAttachmentsService
910
from .jobs_service import JobsService
1011
from .llm_gateway_service import UiPathLlmChatService, UiPathOpenAIService
1112
from .processes_service import ProcessesService
@@ -22,6 +23,7 @@
2223
"ApiClient",
2324
"QueuesService",
2425
"JobsService",
26+
"JobAttachmentsService",
2527
"UiPathOpenAIService",
2628
"UiPathLlmChatService",
2729
"FolderService",
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import os
2+
import shutil
3+
import tempfile
4+
import uuid
5+
from typing import Optional, Union
6+
7+
from .._config import Config
8+
from .._execution_context import ExecutionContext
9+
from .._folder_context import FolderContext
10+
from ._base_service import BaseService
11+
from .attachments_service import AttachmentsService
12+
from .jobs_service import JobsService
13+
14+
15+
class JobAttachmentsService(FolderContext, BaseService):
16+
"""Service for managing job attachments in UiPath Orchestrator, focusing on operations related to the current job context.
17+
18+
Provides methods to attach files to the current job (uploading and linking), and download attachments from the current job.
19+
Delegates direct attachment upload/download to AttachmentsService and linking to JobsService.
20+
Reference: https://docs.uipath.com/orchestrator/reference/api-job-attachments (Note: This service no longer lists attachments directly)
21+
"""
22+
23+
def __init__(self, config: Config, execution_context: ExecutionContext) -> None:
24+
super().__init__(config=config, execution_context=execution_context)
25+
self._attachments_service = AttachmentsService(config, execution_context)
26+
self._jobs_service = JobsService(config, execution_context)
27+
28+
def attach_file_to_current_job(
29+
self,
30+
*,
31+
name: str,
32+
content: Optional[Union[str, bytes]] = None,
33+
source_path: Optional[str] = None,
34+
category: Optional[str] = None,
35+
) -> Union[uuid.UUID, str]:
36+
"""Attach a file to the current job, or write to temp if no job context.
37+
38+
If a job key is present in the execution context, uploads the payload (from content or source_path) using AttachmentsService and links it to the current job using JobsService. Otherwise, writes/copies the file to the system temp directory.
39+
40+
Args:
41+
name (str): The name of the file/attachment.
42+
content (Optional[Union[str, bytes]]): The file content (string or bytes). Mutually exclusive with source_path.
43+
source_path (Optional[str]): The local path of the file to attach. Mutually exclusive with content.
44+
category (Optional[str]): Optional category for the attachment in the context of this job.
45+
46+
Returns:
47+
Union[uuid.UUID, str]: The attachment key if uploaded, or the temp file path if written locally.
48+
49+
Raises:
50+
ValueError: If neither content nor source_path is provided, or if both are provided.
51+
"""
52+
if not (content is not None or source_path is not None):
53+
raise ValueError("Either content or source_path must be provided.")
54+
if content is not None and source_path is not None:
55+
raise ValueError(
56+
"Parameters content and source_path are mutually exclusive."
57+
)
58+
59+
job_key_str = self._execution_context._instance_key
60+
if job_key_str:
61+
attachment_key = self._attachments_service.upload(
62+
name=name, content=content, source_path=source_path
63+
)
64+
self._jobs_service.link_attachment(
65+
attachment_key=attachment_key,
66+
job_key=uuid.UUID(job_key_str),
67+
category=category,
68+
)
69+
return attachment_key
70+
else:
71+
temp_dir = tempfile.gettempdir()
72+
temp_path = os.path.join(temp_dir, name)
73+
if source_path:
74+
shutil.copyfile(source_path, temp_path)
75+
elif content is not None: # content can be str or bytes
76+
mode = "w" if isinstance(content, str) else "wb"
77+
with open(temp_path, mode) as f:
78+
f.write(content)
79+
return temp_path
80+
81+
def download_attachment_from_current_job(
82+
self,
83+
*,
84+
name: str,
85+
destination_path: str,
86+
) -> str:
87+
"""Download an attachment from the current job or from temp storage.
88+
89+
If a job key is present in the execution context, it attempts to find an attachment with the given name by listing attachments for the current job (using JobsService) and then downloads it using AttachmentsService. If no job key, copies the file from the temp directory to the destination path.
90+
91+
Args:
92+
name (str): The name of the attachment file.
93+
destination_path (str): The local path where the attachment will be saved.
94+
95+
Returns:
96+
str: The path to the downloaded file.
97+
98+
Raises:
99+
FileNotFoundError: If the attachment is not found for the current job, or if the temp file does not exist.
100+
"""
101+
job_key_str = self._execution_context._instance_key
102+
if job_key_str:
103+
job_attachments = self._jobs_service.list_attachments(
104+
job_key=uuid.UUID(job_key_str)
105+
)
106+
match = next((a for a in job_attachments if a.name == name), None)
107+
if not match:
108+
raise FileNotFoundError(
109+
f"Attachment '{name}' not found for job {job_key_str}"
110+
)
111+
self._attachments_service.download(
112+
key=match.key, destination_path=destination_path
113+
)
114+
return destination_path
115+
else:
116+
temp_dir = tempfile.gettempdir()
117+
temp_path = os.path.join(temp_dir, name)
118+
if not os.path.exists(temp_path):
119+
raise FileNotFoundError(f"Temp file '{temp_path}' does not exist")
120+
shutil.copyfile(temp_path, destination_path)
121+
return destination_path

src/uipath/_uipath.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
ConnectionsService,
1616
ContextGroundingService,
1717
FolderService,
18+
JobAttachmentsService,
1819
JobsService,
1920
ProcessesService,
2021
QueuesService,
@@ -122,3 +123,8 @@ def folders(self) -> FolderService:
122123
if not self._folders_service:
123124
self._folders_service = FolderService(self._config, self._execution_context)
124125
return self._folders_service
126+
127+
@property
128+
def job_attachments(self) -> JobAttachmentsService:
129+
"""Provides access to job attachment operations."""
130+
return JobAttachmentsService(self._config, self._execution_context)

0 commit comments

Comments
 (0)