Skip to content

Commit 44d557d

Browse files
authored
feat: add timeout resume helpers (#1795)
1 parent db78569 commit 44d557d

13 files changed

Lines changed: 236 additions & 25 deletions

File tree

‎packages/uipath-core/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-core"
3-
version = "0.5.28"
3+
version = "0.5.29"
44
description = "UiPath Core abstractions"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

‎packages/uipath-core/src/uipath/core/triggers/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"UiPathResumeTriggerType",
66
"UiPathApiTrigger",
77
"UiPathIntegrationTrigger",
8+
"UiPathResumeMetadata",
89
"UiPathResumeTriggerName",
910
"UIPATH_METADATA_KEY",
1011
]
@@ -13,6 +14,7 @@
1314
UIPATH_METADATA_KEY,
1415
UiPathApiTrigger,
1516
UiPathIntegrationTrigger,
17+
UiPathResumeMetadata,
1618
UiPathResumeTrigger,
1719
UiPathResumeTriggerName,
1820
UiPathResumeTriggerType,

‎packages/uipath-core/src/uipath/core/triggers/trigger.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ class UiPathResumeTriggerName(str, Enum):
4848
DEEP_RAG_RAW = "DeepRagRaw"
4949

5050

51+
class UiPathResumeMetadata(BaseModel):
52+
"""UiPath metadata attached to resume values from multi-trigger interrupts."""
53+
54+
kind: str | None = None
55+
trigger_type: UiPathResumeTriggerType | None = Field(
56+
default=None, alias="triggerType"
57+
)
58+
trigger_name: UiPathResumeTriggerName | None = Field(
59+
default=None, alias="triggerName"
60+
)
61+
62+
model_config = ConfigDict(validate_by_name=True)
63+
64+
5165
class UiPathApiTrigger(BaseModel):
5266
"""API resume trigger request."""
5367

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from uipath.core.triggers import (
2+
UiPathResumeMetadata,
3+
UiPathResumeTriggerName,
4+
UiPathResumeTriggerType,
5+
)
6+
7+
8+
def test_resume_metadata_accepts_trigger_aliases() -> None:
9+
metadata = UiPathResumeMetadata.model_validate(
10+
{
11+
"kind": "timeout",
12+
"triggerType": "Timer",
13+
"triggerName": "Timer",
14+
}
15+
)
16+
17+
assert metadata.kind == "timeout"
18+
assert metadata.trigger_type == UiPathResumeTriggerType.TIMER
19+
assert metadata.trigger_name == UiPathResumeTriggerName.TIMER
20+
21+
22+
def test_resume_metadata_accepts_field_names() -> None:
23+
metadata = UiPathResumeMetadata(
24+
trigger_type=UiPathResumeTriggerType.API,
25+
trigger_name=UiPathResumeTriggerName.API,
26+
)
27+
28+
assert metadata.trigger_type == UiPathResumeTriggerType.API
29+
assert metadata.trigger_name == UiPathResumeTriggerName.API

‎packages/uipath-core/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.

‎packages/uipath-platform/pyproject.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
[project]
22
name = "uipath-platform"
3-
version = "0.2.1"
3+
version = "0.2.2"
44
description = "HTTP client library for programmatic access to UiPath Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
77
dependencies = [
88
"httpx>=0.28.1",
99
"tenacity>=9.0.0",
1010
"truststore>=0.10.1",
11-
"uipath-core>=0.5.28, <0.6.0",
11+
"uipath-core>=0.5.29, <0.6.0",
1212
"pydantic-function-models>=0.1.11",
1313
"sqlparse>=0.5.5",
1414
]

‎packages/uipath-platform/src/uipath/platform/common/__init__.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
This module contains common models used across multiple services.
44
"""
55

6+
from uipath.core.triggers import UiPathResumeMetadata
7+
68
from ._api_client import ApiClient
79
from ._base_service import BaseService, resolve_trace_id
810
from ._bindings import (
@@ -62,6 +64,12 @@
6264
WaitUntil,
6365
)
6466
from .paging import PagedResult
67+
from .timeout import (
68+
UiPathTimeoutError,
69+
assert_no_timeout,
70+
get_resume_metadata,
71+
is_timeout,
72+
)
6573

6674
__all__ = [
6775
"ApiClient",
@@ -125,6 +133,11 @@
125133
"resolve_service_url",
126134
"inject_routing_headers",
127135
"resolve_trace_id",
136+
"UiPathTimeoutError",
137+
"UiPathResumeMetadata",
138+
"assert_no_timeout",
139+
"get_resume_metadata",
140+
"is_timeout",
128141
]
129142

130143
from .validation import validate_pagination_params
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Helpers for resume values produced by timeout triggers."""
2+
3+
from collections.abc import Mapping
4+
from typing import Any, TypeVar
5+
6+
from pydantic import ValidationError
7+
from uipath.core.triggers import (
8+
UIPATH_METADATA_KEY,
9+
UiPathResumeMetadata,
10+
)
11+
12+
T = TypeVar("T")
13+
14+
_TIMEOUT_KIND = "timeout"
15+
16+
17+
class UiPathTimeoutError(TimeoutError):
18+
"""Raised when a resume value came from a UiPath timeout trigger."""
19+
20+
def __init__(self, value: Any):
21+
"""Create a timeout error that keeps the original resume value."""
22+
super().__init__("UiPath interrupt timed out.")
23+
self.value = value
24+
25+
26+
def is_timeout(value: Any) -> bool:
27+
"""Return True when a resume value came from a UiPath timeout trigger."""
28+
metadata = get_resume_metadata(value)
29+
return metadata is not None and metadata.kind == _TIMEOUT_KIND
30+
31+
32+
def assert_no_timeout(value: T) -> T:
33+
"""Raise UiPathTimeoutError if a resume value came from a timeout trigger."""
34+
if is_timeout(value):
35+
raise UiPathTimeoutError(value)
36+
return value
37+
38+
39+
def get_resume_metadata(value: Any) -> UiPathResumeMetadata | None:
40+
"""Return UiPath resume metadata when present on a resume value."""
41+
metadata = _metadata(value)
42+
if metadata is None:
43+
return None
44+
45+
try:
46+
return UiPathResumeMetadata.model_validate(metadata)
47+
except ValidationError:
48+
return None
49+
50+
51+
def _metadata(value: Any) -> Mapping[str, Any] | None:
52+
if not isinstance(value, Mapping):
53+
return None
54+
55+
metadata = value.get(UIPATH_METADATA_KEY)
56+
return metadata if isinstance(metadata, Mapping) else None
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import pytest
2+
from uipath.core.triggers import (
3+
UIPATH_METADATA_KEY,
4+
UiPathResumeTriggerName,
5+
UiPathResumeTriggerType,
6+
)
7+
8+
from uipath.platform.common import (
9+
UiPathResumeMetadata,
10+
UiPathTimeoutError,
11+
assert_no_timeout,
12+
get_resume_metadata,
13+
is_timeout,
14+
)
15+
16+
17+
def test_is_timeout_detects_timeout_metadata() -> None:
18+
value = {
19+
UIPATH_METADATA_KEY: {
20+
"kind": "timeout",
21+
"triggerType": "Timer",
22+
"triggerName": "Timer",
23+
},
24+
"value": None,
25+
}
26+
27+
assert is_timeout(value) is True
28+
29+
30+
def test_get_resume_metadata_returns_typed_metadata() -> None:
31+
value = {
32+
UIPATH_METADATA_KEY: {
33+
"kind": "timeout",
34+
"triggerType": "Timer",
35+
"triggerName": "Timer",
36+
},
37+
"resumeTime": "2026-07-07T12:00:00Z",
38+
}
39+
40+
metadata = get_resume_metadata(value)
41+
42+
assert isinstance(metadata, UiPathResumeMetadata)
43+
assert metadata.kind == "timeout"
44+
assert metadata.trigger_type == UiPathResumeTriggerType.TIMER
45+
assert metadata.trigger_name == UiPathResumeTriggerName.TIMER
46+
47+
48+
def test_get_resume_metadata_returns_none_without_metadata() -> None:
49+
assert get_resume_metadata({"result": "done"}) is None
50+
assert get_resume_metadata("done") is None
51+
52+
53+
def test_get_resume_metadata_returns_none_for_invalid_metadata() -> None:
54+
value = {
55+
UIPATH_METADATA_KEY: {
56+
"triggerType": "NotARealTrigger",
57+
"triggerName": "Timer",
58+
},
59+
}
60+
61+
assert get_resume_metadata(value) is None
62+
63+
64+
def test_is_timeout_ignores_plain_timer_metadata() -> None:
65+
value = {
66+
UIPATH_METADATA_KEY: {
67+
"triggerType": "Timer",
68+
"triggerName": "Timer",
69+
},
70+
"resumeTime": "2026-07-07T12:00:00Z",
71+
}
72+
73+
assert is_timeout(value) is False
74+
75+
76+
def test_is_timeout_ignores_user_timed_out_fields() -> None:
77+
assert is_timeout({"timedOut": True}) is False
78+
assert is_timeout("timeout") is False
79+
80+
81+
def test_assert_no_timeout_returns_original_value() -> None:
82+
value = {"result": "done"}
83+
84+
assert assert_no_timeout(value) is value
85+
86+
87+
def test_assert_no_timeout_raises_with_resume_value() -> None:
88+
value = {
89+
UIPATH_METADATA_KEY: {"kind": "timeout"},
90+
"value": {"jobKey": "job-1"},
91+
}
92+
93+
with pytest.raises(UiPathTimeoutError) as exc_info:
94+
assert_no_timeout(value)
95+
96+
assert exc_info.value.value is value

‎packages/uipath-platform/uv.lock‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)