|
| 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 |
0 commit comments