Skip to content

Commit 340f877

Browse files
committed
feat: extract common methods
1 parent d2853a2 commit 340f877

File tree

5 files changed

+280
-1
lines changed

5 files changed

+280
-1
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.0.62"
3+
version = "2.0.63"
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.10"
@@ -93,6 +93,7 @@ ignore-decorators = []
9393
"uipath/_cli/**" = ["D"]
9494
# TODO: Remove this once model documentation is added
9595
"src/uipath/models/**" = ["D101", "D100", "D104", "D102", "D107"]
96+
"src/uipath/runtime_commons/**" = ["D101", "D100", "D104", "D102", "D107", "D103"]
9697

9798
[tool.ruff.format]
9899
quote-style = "double"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .hitl import HitlProcessor, HitlReader
2+
from .tracing import setup_tracer_httpx_logging, simple_serialize_defaults
3+
4+
__all__ = [
5+
"HitlReader",
6+
"HitlProcessor",
7+
"setup_tracer_httpx_logging",
8+
"simple_serialize_defaults",
9+
]

src/uipath/runtime_commons/hitl.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import json
2+
import uuid
3+
from dataclasses import dataclass
4+
from functools import cached_property
5+
from typing import Any, Optional
6+
7+
from uipath import UiPath
8+
from uipath.models import CreateAction, InvokeProcess, WaitAction, WaitJob
9+
10+
from .._cli._runtime._contracts import (
11+
UiPathApiTrigger,
12+
UiPathErrorCategory,
13+
UiPathResumeTrigger,
14+
UiPathResumeTriggerType,
15+
UiPathRuntimeError,
16+
UiPathRuntimeStatus,
17+
)
18+
19+
20+
def _try_convert_to_json_format(value: str) -> str:
21+
try:
22+
return json.loads(value)
23+
except json.decoder.JSONDecodeError:
24+
return value
25+
26+
27+
async def _get_api_payload(inbox_id: str) -> Any:
28+
"""Fetch payload data for API triggers.
29+
30+
Args:
31+
inbox_id: The Id of the inbox to fetch the payload for.
32+
33+
Returns:
34+
The value field from the API response payload, or None if an error occurs.
35+
"""
36+
response = None
37+
try:
38+
uipath = UiPath()
39+
response = uipath.api_client.request(
40+
"GET",
41+
f"/orchestrator_/api/JobTriggers/GetPayload/{inbox_id}",
42+
include_folder_headers=True,
43+
)
44+
data = response.json()
45+
return data.get("payload")
46+
except Exception as e:
47+
raise UiPathRuntimeError(
48+
"API_CONNECTION_ERROR",
49+
"Failed to get trigger payload",
50+
f"Error fetching API trigger payload for inbox {inbox_id}: {str(e)}",
51+
UiPathErrorCategory.SYSTEM,
52+
response.status_code if response else None,
53+
) from e
54+
55+
56+
class HitlReader:
57+
@classmethod
58+
async def read(cls, resume_trigger: UiPathResumeTrigger) -> Optional[str]:
59+
uipath = UiPath()
60+
match resume_trigger.trigger_type:
61+
case UiPathResumeTriggerType.ACTION:
62+
if resume_trigger.item_key:
63+
action = await uipath.actions.retrieve_async(
64+
resume_trigger.item_key,
65+
app_folder_key=resume_trigger.folder_key,
66+
app_folder_path=resume_trigger.folder_path,
67+
)
68+
return action.data
69+
70+
case UiPathResumeTriggerType.JOB:
71+
if resume_trigger.item_key:
72+
job = await uipath.jobs.retrieve_async(
73+
resume_trigger.item_key,
74+
folder_key=resume_trigger.folder_key,
75+
folder_path=resume_trigger.folder_path,
76+
)
77+
if (
78+
job.state
79+
and not job.state.lower()
80+
== UiPathRuntimeStatus.SUCCESSFUL.value.lower()
81+
):
82+
raise UiPathRuntimeError(
83+
"INVOKED_PROCESS_FAILURE",
84+
"Invoked process did not finish successfully.",
85+
_try_convert_to_json_format(str(job.job_error or job.info)),
86+
)
87+
return job.output_arguments
88+
89+
case UiPathResumeTriggerType.API:
90+
if resume_trigger.api_resume and resume_trigger.api_resume.inbox_id:
91+
return await _get_api_payload(resume_trigger.api_resume.inbox_id)
92+
93+
case _:
94+
raise UiPathRuntimeError(
95+
"UNKNOWN_TRIGGER_TYPE",
96+
"Unexpected trigger type received",
97+
f"Trigger type :{type(resume_trigger.trigger_type)} is invalid",
98+
UiPathErrorCategory.USER,
99+
)
100+
101+
raise UiPathRuntimeError(
102+
"HITL_FEEDBACK_FAILURE",
103+
"Failed to receive payload from HITL action",
104+
detail="Failed to receive payload from HITL action",
105+
category=UiPathErrorCategory.SYSTEM,
106+
)
107+
108+
109+
@dataclass
110+
class HitlProcessor:
111+
"""Processes events in a Human-(Robot/Agent)-In-The-Loop scenario."""
112+
113+
value: Any
114+
115+
@cached_property
116+
def type(self) -> UiPathResumeTriggerType:
117+
"""Returns the type of the interrupt value."""
118+
if isinstance(self.value, CreateAction) or isinstance(self.value, WaitAction):
119+
return UiPathResumeTriggerType.ACTION
120+
if isinstance(self.value, InvokeProcess) or isinstance(self.value, WaitJob):
121+
return UiPathResumeTriggerType.JOB
122+
# default to API trigger
123+
return UiPathResumeTriggerType.API
124+
125+
async def create_resume_trigger(self) -> Optional[UiPathResumeTrigger]:
126+
"""Returns the resume trigger."""
127+
uipath = UiPath()
128+
try:
129+
hitl_input = self.value
130+
resume_trigger = UiPathResumeTrigger(
131+
trigger_type=self.type, payload=hitl_input.model_dump_json()
132+
)
133+
match self.type:
134+
case UiPathResumeTriggerType.ACTION:
135+
resume_trigger.folder_path = hitl_input.app_folder_path
136+
resume_trigger.folder_key = hitl_input.app_folder_key
137+
if isinstance(hitl_input, WaitAction):
138+
resume_trigger.item_key = hitl_input.action.key
139+
elif isinstance(hitl_input, CreateAction):
140+
action = await uipath.actions.create_async(
141+
title=hitl_input.title,
142+
app_name=hitl_input.app_name if hitl_input.app_name else "",
143+
app_folder_path=hitl_input.app_folder_path
144+
if hitl_input.app_folder_path
145+
else "",
146+
app_folder_key=hitl_input.app_folder_key
147+
if hitl_input.app_folder_key
148+
else "",
149+
app_key=hitl_input.app_key if hitl_input.app_key else "",
150+
app_version=hitl_input.app_version
151+
if hitl_input.app_version
152+
else 1,
153+
assignee=hitl_input.assignee if hitl_input.assignee else "",
154+
data=hitl_input.data,
155+
)
156+
if action:
157+
resume_trigger.item_key = action.key
158+
159+
case UiPathResumeTriggerType.JOB:
160+
resume_trigger.folder_path = hitl_input.process_folder_path
161+
resume_trigger.folder_key = hitl_input.process_folder_key
162+
if isinstance(hitl_input, WaitJob):
163+
resume_trigger.item_key = hitl_input.job.key
164+
elif isinstance(hitl_input, InvokeProcess):
165+
job = await uipath.processes.invoke_async(
166+
name=hitl_input.name,
167+
input_arguments=hitl_input.input_arguments,
168+
folder_path=hitl_input.process_folder_path,
169+
folder_key=hitl_input.process_folder_key,
170+
)
171+
if job:
172+
resume_trigger.item_key = job.key
173+
174+
case UiPathResumeTriggerType.API:
175+
resume_trigger.api_resume = UiPathApiTrigger(
176+
inbox_id=str(uuid.uuid4()), request=hitl_input.prefix
177+
)
178+
case _:
179+
raise UiPathRuntimeError(
180+
"UNKNOWN_HITL_MODEL",
181+
"Unexpected model received",
182+
f"{type(hitl_input)} is not a valid Human(Robot/Agent)-In-The-Loop model",
183+
UiPathErrorCategory.USER,
184+
)
185+
except Exception as e:
186+
raise UiPathRuntimeError(
187+
"HITL_ACTION_CREATION_FAILED",
188+
"Failed to create HITL action",
189+
f"{str(e)}",
190+
UiPathErrorCategory.SYSTEM,
191+
) from e
192+
193+
return resume_trigger

src/uipath/runtime_commons/tracing.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import datetime
2+
import logging
3+
from zoneinfo import ZoneInfo
4+
5+
6+
class IgnoreSpecificUrl(logging.Filter):
7+
def __init__(self, url_to_ignore):
8+
super().__init__()
9+
self.url_to_ignore = url_to_ignore
10+
11+
def filter(self, record):
12+
try:
13+
if record.msg == 'HTTP Request: %s %s "%s %d %s"':
14+
# Ignore the log if the URL matches the one we want to ignore
15+
method = record.args[0]
16+
url = record.args[1]
17+
18+
if method == "POST" and url.path.endswith(self.url_to_ignore):
19+
# Check if the URL contains the specific path we want to ignore
20+
return True
21+
return False
22+
23+
except Exception:
24+
return False
25+
26+
27+
def setup_tracer_httpx_logging(url: str):
28+
# Create a custom logger for httpx
29+
# Add the custom filter to the root logger
30+
logging.getLogger("httpx").addFilter(IgnoreSpecificUrl(url))
31+
32+
33+
def simple_serialize_defaults(obj):
34+
if hasattr(obj, "model_dump"):
35+
return obj.model_dump(exclude_none=True, mode="json")
36+
if hasattr(obj, "dict"):
37+
return obj.dict()
38+
if hasattr(obj, "to_dict"):
39+
return obj.to_dict()
40+
41+
if isinstance(obj, (set, tuple)):
42+
if hasattr(obj, "_asdict") and callable(obj._asdict):
43+
return obj._asdict()
44+
return list(obj)
45+
46+
if isinstance(obj, datetime.datetime):
47+
return obj.isoformat()
48+
49+
if isinstance(obj, (datetime.timezone, ZoneInfo)):
50+
return obj.tzname(None)
51+
52+
return str(obj)

src/uipath/runtime_commons/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
def serialize_object(obj):
2+
"""Recursively serializes an object and all its nested components."""
3+
# Handle Pydantic models
4+
if hasattr(obj, "model_dump"):
5+
return serialize_object(obj.model_dump(by_alias=True))
6+
elif hasattr(obj, "dict"):
7+
return serialize_object(obj.dict())
8+
elif hasattr(obj, "to_dict"):
9+
return serialize_object(obj.to_dict())
10+
# Handle dictionaries
11+
elif isinstance(obj, dict):
12+
return {k: serialize_object(v) for k, v in obj.items()}
13+
# Handle lists
14+
elif isinstance(obj, list):
15+
return [serialize_object(item) for item in obj]
16+
# Handle other iterable objects (convert to dict first)
17+
elif hasattr(obj, "__iter__") and not isinstance(obj, (str, bytes)):
18+
try:
19+
return serialize_object(dict(obj))
20+
except (TypeError, ValueError):
21+
return obj
22+
# Return primitive types as is
23+
else:
24+
return obj

0 commit comments

Comments
 (0)