Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[project]
name = "uipath"
version = "2.2.12"
version = "2.2.13"
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
dependencies = [
"uipath-core>=0.0.6, <0.1.0",
"uipath-core>=0.0.9, <0.1.0",
"uipath-runtime>=0.1.1, <0.2.0",
"click>=8.3.1",
"httpx>=0.28.1",
Expand Down
26 changes: 22 additions & 4 deletions src/uipath/platform/action_center/_tasks_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,16 +303,25 @@ def create(
return Task.model_validate(json_response)

@traced(name="tasks_retrieve", run_type="uipath")
@resource_override(
resource_type="app",
resource_identifier="app_name",
folder_identifier="app_folder_path",
)
def retrieve(
self, action_key: str, app_folder_path: str = "", app_folder_key: str = ""
self,
action_key: str,
app_folder_path: str = "",
app_folder_key: str = "",
app_name: str | None = None,
) -> Task:
"""Retrieves a task by its key synchronously.

Args:
action_key: The unique identifier of the task to retrieve
app_folder_path: Optional folder path for the task
app_folder_key: Optional folder key for the task

app_name: app name hint for resource override
Returns:
Task: The retrieved task object
"""
Expand All @@ -328,16 +337,25 @@ def retrieve(
return Task.model_validate(response.json())

@traced(name="tasks_retrieve", run_type="uipath")
@resource_override(
resource_type="app",
resource_identifier="app_name",
folder_identifier="app_folder_path",
)
async def retrieve_async(
self, action_key: str, app_folder_path: str = "", app_folder_key: str = ""
self,
action_key: str,
app_folder_path: str = "",
app_folder_key: str = "",
app_name: str | None = None,
) -> Task:
"""Retrieves a task by its key asynchronously.

Args:
action_key: The unique identifier of the task to retrieve
app_folder_path: Optional folder path for the task
app_folder_key: Optional folder key for the task

app_name: app name hint for resource override
Returns:
Task: The retrieved task object
"""
Expand Down
10 changes: 10 additions & 0 deletions src/uipath/platform/action_center/tasks.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
"""Data model for an Action in the UiPath Platform."""

import enum
from datetime import datetime
from typing import Any, Dict, List, Optional, Union

from pydantic import BaseModel, ConfigDict, Field, field_serializer


class TaskStatus(enum.IntEnum):
"""Enum representing possible Task status."""

UNASSIGNED = 0
PENDING = 1
COMPLETED = 2


class Task(BaseModel):
"""Model representing a Task in the UiPath Platform."""

Expand All @@ -29,6 +38,7 @@ def serialize_datetime(self, value):
)
app_tasks_metadata: Optional[Any] = Field(default=None, alias="appTasksMetadata")
action_label: Optional[str] = Field(default=None, alias="actionLabel")
# 2.3.0 change to TaskStatus enum
status: Optional[Union[str, int]] = None
data: Optional[Dict[str, Any]] = None
action: Optional[str] = None
Expand Down
18 changes: 13 additions & 5 deletions src/uipath/platform/orchestrator/_jobs_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from ..._config import Config
from ..._execution_context import ExecutionContext
from ..._folder_context import FolderContext
from ..._utils import Endpoint, RequestSpec, header_folder
from ..._utils import Endpoint, RequestSpec, header_folder, resource_override
from ..._utils.constants import TEMP_ATTACHMENTS_FOLDER
from ...tracing import traced
from ..common._base_service import BaseService
Expand Down Expand Up @@ -148,19 +148,23 @@ async def main(): # noqa: D103
def custom_headers(self) -> Dict[str, str]:
return self.folder_headers

@traced(name="jobs_retrieve", run_type="uipath")
@resource_override(resource_type="process", resource_identifier="process_name")
def retrieve(
self,
job_key: str,
*,
folder_key: Optional[str] = None,
folder_path: Optional[str] = None,
folder_key: str | None = None,
folder_path: str | None = None,
process_name: str | None = None,
) -> Job:
"""Retrieve a job identified by its key.

Args:
job_key (str): The job unique identifier.
folder_key (Optional[str]): The key of the folder in which the job was executed.
folder_path (Optional[str]): The path of the folder in which the job was executed.
process_name: process name hint for resource override

Returns:
Job: The retrieved job.
Expand All @@ -184,19 +188,23 @@ def retrieve(

return Job.model_validate(response.json())

@traced(name="jobs_retrieve_async", run_type="uipath")
@resource_override(resource_type="process", resource_identifier="process_name")
async def retrieve_async(
self,
job_key: str,
*,
folder_key: Optional[str] = None,
folder_path: Optional[str] = None,
folder_key: str | None = None,
folder_path: str | None = None,
process_name: str | None = None,
) -> Job:
"""Asynchronously retrieve a job identified by its key.

Args:
job_key (str): The job unique identifier.
folder_key (Optional[str]): The key of the folder in which the job was executed.
folder_path (Optional[str]): The path of the folder in which the job was executed.
process_name: process name hint for resource override

Returns:
Job: The retrieved job.
Expand Down
12 changes: 12 additions & 0 deletions src/uipath/platform/orchestrator/job.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
"""Models for Orchestrator Jobs."""

from enum import Enum
from typing import Any, Dict, Optional

from pydantic import BaseModel, ConfigDict, Field


class JobState(str, Enum):
"""Job state enum."""

SUCCESSFUL = "successful"
FAULTED = "faulted"
SUSPENDED = "suspended"
RUNNING = "running"
PENDING = "pending"


class JobErrorInfo(BaseModel):
"""Model representing job error information."""

Expand Down Expand Up @@ -35,6 +46,7 @@ class Job(BaseModel):
key: Optional[str] = Field(default=None, alias="Key")
start_time: Optional[str] = Field(default=None, alias="StartTime")
end_time: Optional[str] = Field(default=None, alias="EndTime")
# 2.3.0 change to JobState enum
state: Optional[str] = Field(default=None, alias="State")
job_priority: Optional[str] = Field(default=None, alias="JobPriority")
specific_priority_value: Optional[int] = Field(
Expand Down
125 changes: 79 additions & 46 deletions src/uipath/platform/resume_triggers/_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@
import uuid
from typing import Any

from uipath.core.errors import (
ErrorCategory,
UiPathFaultedTriggerError,
UiPathPendingTriggerError,
)
from uipath.runtime import (
UiPathApiTrigger,
UiPathResumeTrigger,
UiPathResumeTriggerName,
UiPathResumeTriggerType,
UiPathRuntimeStatus,
)
from uipath.runtime.errors import (
UiPathErrorCategory,
UiPathErrorCode,
UiPathRuntimeError,
)

from uipath._cli._utils._common import serialize_object
from uipath.platform import UiPath
from uipath.platform.action_center import Task
from uipath.platform.action_center.tasks import TaskStatus
from uipath.platform.common import (
CreateEscalation,
CreateTask,
Expand All @@ -28,6 +28,7 @@
WaitJob,
WaitTask,
)
from uipath.platform.orchestrator.job import JobState


def _try_convert_to_json_format(value: str | None) -> str | None:
Expand All @@ -53,6 +54,20 @@ class UiPathResumeTriggerReader:
Implements UiPathResumeTriggerReaderProtocol.
"""

def _extract_name_hint(self, field_name: str, payload: Any) -> str | None:
if not payload:
return payload

if isinstance(payload, dict):
return payload.get(field_name)

# 2.3.0 remove
try:
payload_dict = json.loads(payload)
return payload_dict.get(field_name)
except json.decoder.JSONDecodeError:
return None

async def read_trigger(self, trigger: UiPathResumeTrigger) -> Any | None:
"""Read a resume trigger and convert it to runtime-compatible input.

Expand Down Expand Up @@ -80,39 +95,66 @@ async def read_trigger(self, trigger: UiPathResumeTrigger) -> Any | None:
match trigger.trigger_type:
case UiPathResumeTriggerType.TASK:
if trigger.item_key:
action: Task = await uipath.tasks.retrieve_async(
task: Task = await uipath.tasks.retrieve_async(
trigger.item_key,
app_folder_key=trigger.folder_key,
app_folder_path=trigger.folder_path,
app_name=self._extract_name_hint("app_name", trigger.payload),
)
pending_status = TaskStatus.PENDING.value
unassigned_status = TaskStatus.UNASSIGNED.value

if task.status in (pending_status, unassigned_status):
# 2.3.0 remove (task.status will already be the enum)
current_status = (
TaskStatus(task.status).name
if isinstance(task.status, int)
else "Unknown"
)
raise UiPathPendingTriggerError(
ErrorCategory.SYSTEM,
f"Task is not completed yet. Current status: {current_status}",
)

if trigger.trigger_name == UiPathResumeTriggerName.ESCALATION:
return action
return task

return action.data
return task.data

case UiPathResumeTriggerType.JOB:
if trigger.item_key:
job = await uipath.jobs.retrieve_async(
trigger.item_key,
folder_key=trigger.folder_key,
folder_path=trigger.folder_path,
process_name=self._extract_name_hint("name", trigger.payload),
)
job_state = (job.state or "").lower()
successful_state = UiPathRuntimeStatus.SUCCESSFUL.value.lower()
faulted_state = UiPathRuntimeStatus.FAULTED.value.lower()

if job_state == successful_state:
output_data = await uipath.jobs.extract_output_async(job)
return _try_convert_to_json_format(output_data)

raise UiPathRuntimeError(
UiPathErrorCode.INVOKED_PROCESS_FAILURE,
"Invoked process did not finish successfully.",
_try_convert_to_json_format(str(job.job_error or job.info))
or "Job error unavailable."
if job_state == faulted_state
else f"Job {job.key} is {job_state}.",
)
successful_state = JobState.SUCCESSFUL.value
faulted_state = JobState.FAULTED.value
running_state = JobState.RUNNING.value
pending_state = JobState.PENDING.value

if job_state in (pending_state, running_state):
raise UiPathPendingTriggerError(
ErrorCategory.SYSTEM,
f"Job is not finished yet. Current state: {job_state}",
)

if job_state != successful_state:
job_error = (
_try_convert_to_json_format(str(job.job_error or job.info))
or "Job error unavailable."
if job_state == faulted_state
else f"Job {job.key} is {job_state}."
)
raise UiPathFaultedTriggerError(
ErrorCategory.USER,
f"Process did not finish successfully. Error: {job_error}",
)

output_data = await uipath.jobs.extract_output_async(job)
return _try_convert_to_json_format(output_data)

case UiPathResumeTriggerType.API:
if trigger.api_resume and trigger.api_resume.inbox_id:
Expand All @@ -121,25 +163,20 @@ async def read_trigger(self, trigger: UiPathResumeTrigger) -> Any | None:
trigger.api_resume.inbox_id
)
except Exception as e:
raise UiPathRuntimeError(
UiPathErrorCode.RETRIEVE_RESUME_TRIGGER_ERROR,
"Failed to get trigger payload",
raise UiPathFaultedTriggerError(
ErrorCategory.SYSTEM,
f"Failed to get trigger payload"
f"Error fetching API trigger payload for inbox {trigger.api_resume.inbox_id}: {str(e)}",
UiPathErrorCategory.SYSTEM,
) from e
case _:
raise UiPathRuntimeError(
UiPathErrorCode.UNKNOWN_TRIGGER_TYPE,
"Unexpected trigger type received",
raise UiPathFaultedTriggerError(
ErrorCategory.SYSTEM,
f"Unexpected trigger type received"
f"Trigger type :{type(trigger.trigger_type)} is invalid",
UiPathErrorCategory.USER,
)

raise UiPathRuntimeError(
UiPathErrorCode.RETRIEVE_RESUME_TRIGGER_ERROR,
"Failed to receive payload from HITL action",
detail="Failed to receive payload from HITL action",
category=UiPathErrorCategory.SYSTEM,
raise UiPathFaultedTriggerError(
ErrorCategory.SYSTEM, "Failed to receive payload from HITL action"
)


Expand Down Expand Up @@ -198,20 +235,16 @@ async def create_trigger(self, suspend_value: Any) -> UiPathResumeTrigger:
self._handle_api_trigger(suspend_value, resume_trigger)

case _:
raise UiPathRuntimeError(
UiPathErrorCode.UNKNOWN_HITL_MODEL,
"Unexpected model received",
raise UiPathFaultedTriggerError(
ErrorCategory.SYSTEM,
f"Unexpected model received"
f"{type(suspend_value)} is not a valid Human-In-The-Loop model",
UiPathErrorCategory.USER,
)
except UiPathRuntimeError:
raise
except Exception as e:
raise UiPathRuntimeError(
UiPathErrorCode.CREATE_RESUME_TRIGGER_ERROR,
raise UiPathFaultedTriggerError(
ErrorCategory.SYSTEM,
"Failed to create HITL action",
f"{str(e)}",
UiPathErrorCategory.SYSTEM,
) from e

return resume_trigger
Expand Down
Loading