From 81d246acec8e2a580d1660a246788b3f175a620e Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Mon, 1 Dec 2025 19:14:20 +0200 Subject: [PATCH 1/4] fix: use bindings overwrites on resume trigger data --- src/uipath/_utils/_bindings.py | 45 ++++++ .../platform/resume_triggers/_protocol.py | 44 +++-- tests/sdk/test_bindings.py | 152 ++++++++++++++++++ 3 files changed, 229 insertions(+), 12 deletions(-) diff --git a/src/uipath/_utils/_bindings.py b/src/uipath/_utils/_bindings.py index b92a48c9b..eb10d9e2c 100644 --- a/src/uipath/_utils/_bindings.py +++ b/src/uipath/_utils/_bindings.py @@ -197,3 +197,48 @@ def get_inferred_bindings_names(cls: T): inferred_bindings[name] = method._infer_bindings_mappings # type: ignore # probably a better way to do this return inferred_bindings + + +def resolve_folder_from_bindings( + resource_type: str, + resource_name: Optional[str], + folder_path: Optional[str] = None, +) -> tuple[Optional[str], Optional[str]]: + """Resolve folder path and key from bindings context. + + This function looks up the bindings context variable to find the folder + information for a given resource. + + Args: + resource_type: The type of resource (e.g., "app", "process", "index") + resource_name: The name/identifier of the resource + folder_path: Optional current folder path for more specific matching + + Returns: + Tuple of (folder_path, folder_key) from bindings. Returns (None, None) + if no bindings context is available or no matching resource is found. + """ + if not resource_name: + return None, None + + context_overwrites = _resource_overwrites.get() + if context_overwrites is None: + return None, None + + key = f"{resource_type}.{resource_name}" + # try to apply folder path, fallback to resource_type.resource_name + if folder_path: + key = ( + f"{key}.{folder_path}" + if f"{key}.{folder_path}" in context_overwrites + else key + ) + + matched = context_overwrites.get(key) + if matched is None: + return None, None + + if isinstance(matched, ConnectionResourceOverwrite): + return None, matched.folder_identifier + + return matched.folder_identifier, None diff --git a/src/uipath/platform/resume_triggers/_protocol.py b/src/uipath/platform/resume_triggers/_protocol.py index 303c16a96..34c2bb6ed 100644 --- a/src/uipath/platform/resume_triggers/_protocol.py +++ b/src/uipath/platform/resume_triggers/_protocol.py @@ -4,6 +4,18 @@ import uuid from typing import Any +from uipath._cli._utils._common import serialize_object +from uipath._utils._bindings import resolve_folder_from_bindings +from uipath.platform import UiPath +from uipath.platform.action_center import Task +from uipath.platform.common import ( + CreateEscalation, + CreateTask, + InvokeProcess, + WaitEscalation, + WaitJob, + WaitTask, +) from uipath.runtime import ( UiPathApiTrigger, UiPathResumeTrigger, @@ -17,18 +29,6 @@ UiPathRuntimeError, ) -from uipath._cli._utils._common import serialize_object -from uipath.platform import UiPath -from uipath.platform.action_center import Task -from uipath.platform.common import ( - CreateEscalation, - CreateTask, - InvokeProcess, - WaitEscalation, - WaitJob, - WaitTask, -) - def _try_convert_to_json_format(value: str | None) -> str | None: """Attempts to parse a string as JSON and returns the parsed object or original string. @@ -266,6 +266,16 @@ async def _handle_task_trigger( if isinstance(value, (WaitTask, WaitEscalation)): resume_trigger.item_key = value.action.key elif isinstance(value, (CreateTask, CreateEscalation)): + resolved_path, resolved_key = resolve_folder_from_bindings( + resource_type="app", + resource_name=value.app_name, + folder_path=value.app_folder_path, + ) + if resolved_path: + resume_trigger.folder_path = resolved_path + if resolved_key: + resume_trigger.folder_key = resolved_key + action = await uipath.tasks.create_async( title=value.title, app_name=value.app_name if value.app_name else "", @@ -295,6 +305,16 @@ async def _handle_job_trigger( if isinstance(value, WaitJob): resume_trigger.item_key = value.job.key elif isinstance(value, InvokeProcess): + resolved_path, resolved_key = resolve_folder_from_bindings( + resource_type="process", + resource_name=value.name, + folder_path=value.process_folder_path, + ) + if resolved_path: + resume_trigger.folder_path = resolved_path + if resolved_key: + resume_trigger.folder_key = resolved_key + job = await uipath.processes.invoke_async( name=value.name, input_arguments=value.input_arguments, diff --git a/tests/sdk/test_bindings.py b/tests/sdk/test_bindings.py index 973dedf6c..29503097d 100644 --- a/tests/sdk/test_bindings.py +++ b/tests/sdk/test_bindings.py @@ -4,9 +4,11 @@ from uipath._utils import resource_override from uipath._utils._bindings import ( + ConnectionResourceOverwrite, GenericResourceOverwrite, ResourceOverwritesContext, _resource_overwrites, + resolve_folder_from_bindings, ) @@ -335,3 +337,153 @@ def get_asset(name, folder_path): # Verify context was cleaned up despite the exception result_after = get_asset("test", "original") assert result_after == ("test", "original") + + +class TestResolveFolderFromBindings: + """Tests for the resolve_folder_from_bindings utility function.""" + + def test_returns_none_when_no_context(self): + """Test that resolve_folder_from_bindings returns (None, None) when context is not set.""" + result = resolve_folder_from_bindings("app", "my_app") + assert result == (None, None) + + def test_returns_none_when_resource_name_is_none(self): + """Test that resolve_folder_from_bindings returns (None, None) when resource_name is None.""" + overwrites = { + "app.my_app": GenericResourceOverwrite( + resource_type="app", name="new_app", folder_path="new_folder" + ) + } + token = _resource_overwrites.set(overwrites) + try: + result = resolve_folder_from_bindings("app", None) + assert result == (None, None) + finally: + _resource_overwrites.reset(token) + + def test_returns_none_when_resource_name_is_empty(self): + """Test that resolve_folder_from_bindings returns (None, None) when resource_name is empty.""" + overwrites = { + "app.my_app": GenericResourceOverwrite( + resource_type="app", name="new_app", folder_path="new_folder" + ) + } + token = _resource_overwrites.set(overwrites) + try: + result = resolve_folder_from_bindings("app", "") + assert result == (None, None) + finally: + _resource_overwrites.reset(token) + + def test_returns_folder_path_for_generic_overwrite(self): + """Test that resolve_folder_from_bindings returns folder_path for GenericResourceOverwrite.""" + overwrites = { + "app.my_app": GenericResourceOverwrite( + resource_type="app", name="new_app", folder_path="new_folder" + ) + } + token = _resource_overwrites.set(overwrites) + try: + result = resolve_folder_from_bindings("app", "my_app") + assert result == ("new_folder", None) + finally: + _resource_overwrites.reset(token) + + def test_returns_folder_key_for_connection_overwrite(self): + """Test that resolve_folder_from_bindings returns folder_key for ConnectionResourceOverwrite.""" + overwrites = { + "connection.my_connection": ConnectionResourceOverwrite( + resource_type="connection", + connection_id="conn_123", + element_instance_id="elem_456", + folder_key="folder_key_789", + ) + } + token = _resource_overwrites.set(overwrites) + try: + result = resolve_folder_from_bindings("connection", "my_connection") + assert result == (None, "folder_key_789") + finally: + _resource_overwrites.reset(token) + + def test_returns_none_when_no_matching_key(self): + """Test that resolve_folder_from_bindings returns (None, None) when no matching key exists.""" + overwrites = { + "app.other_app": GenericResourceOverwrite( + resource_type="app", name="new_app", folder_path="new_folder" + ) + } + token = _resource_overwrites.set(overwrites) + try: + result = resolve_folder_from_bindings("app", "my_app") + assert result == (None, None) + finally: + _resource_overwrites.reset(token) + + def test_prefers_specific_key_with_folder_path(self): + """Test that resolve_folder_from_bindings prefers specific key with folder_path.""" + overwrites = { + "app.my_app": GenericResourceOverwrite( + resource_type="app", name="generic_app", folder_path="generic_folder" + ), + "app.my_app.specific_folder": GenericResourceOverwrite( + resource_type="app", name="specific_app", folder_path="specific_folder" + ), + } + token = _resource_overwrites.set(overwrites) + try: + result = resolve_folder_from_bindings("app", "my_app", "specific_folder") + assert result == ("specific_folder", None) + finally: + _resource_overwrites.reset(token) + + def test_falls_back_to_generic_key_when_specific_not_found(self): + """Test that resolve_folder_from_bindings falls back to generic key.""" + overwrites = { + "app.my_app": GenericResourceOverwrite( + resource_type="app", name="generic_app", folder_path="generic_folder" + ), + } + token = _resource_overwrites.set(overwrites) + try: + result = resolve_folder_from_bindings("app", "my_app", "unknown_folder") + assert result == ("generic_folder", None) + finally: + _resource_overwrites.reset(token) + + def test_works_with_process_resource_type(self): + """Test that resolve_folder_from_bindings works with process resource type.""" + overwrites = { + "process.my_process": GenericResourceOverwrite( + resource_type="process", + name="new_process", + folder_path="process_folder", + ) + } + token = _resource_overwrites.set(overwrites) + try: + result = resolve_folder_from_bindings("process", "my_process") + assert result == ("process_folder", None) + finally: + _resource_overwrites.reset(token) + + @pytest.mark.anyio + async def test_works_within_resource_overwrites_context(self): + """Test that resolve_folder_from_bindings works within ResourceOverwritesContext.""" + + async def get_overwrites(): + return { + "app.my_app": GenericResourceOverwrite( + resource_type="app", + name="resolved_app", + folder_path="resolved_folder", + ) + } + + async with ResourceOverwritesContext(get_overwrites): + result = resolve_folder_from_bindings("app", "my_app") + assert result == ("resolved_folder", None) + + # Context should be cleaned up + result_after = resolve_folder_from_bindings("app", "my_app") + assert result_after == (None, None) From 8c93974c7a94e1d3dbf084bd2ce5ca8130c3eb1c Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Mon, 1 Dec 2025 22:08:57 +0200 Subject: [PATCH 2/4] chore: pkg version bump --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index faa1389c6..f852d47fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.2.8" +version = "2.2.9" 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" diff --git a/uv.lock b/uv.lock index d1ff7d291..0617906ff 100644 --- a/uv.lock +++ b/uv.lock @@ -2401,7 +2401,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.2.8" +version = "2.2.9" source = { editable = "." } dependencies = [ { name = "click" }, From bd517ba85d5a440b5e2c58239fc1266ff2aeddbe Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Mon, 1 Dec 2025 22:39:47 +0200 Subject: [PATCH 3/4] chore: ruff check fix --- .../platform/resume_triggers/_protocol.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/uipath/platform/resume_triggers/_protocol.py b/src/uipath/platform/resume_triggers/_protocol.py index 34c2bb6ed..f65240607 100644 --- a/src/uipath/platform/resume_triggers/_protocol.py +++ b/src/uipath/platform/resume_triggers/_protocol.py @@ -4,18 +4,6 @@ import uuid from typing import Any -from uipath._cli._utils._common import serialize_object -from uipath._utils._bindings import resolve_folder_from_bindings -from uipath.platform import UiPath -from uipath.platform.action_center import Task -from uipath.platform.common import ( - CreateEscalation, - CreateTask, - InvokeProcess, - WaitEscalation, - WaitJob, - WaitTask, -) from uipath.runtime import ( UiPathApiTrigger, UiPathResumeTrigger, @@ -29,6 +17,19 @@ UiPathRuntimeError, ) +from uipath._cli._utils._common import serialize_object +from uipath._utils._bindings import resolve_folder_from_bindings +from uipath.platform import UiPath +from uipath.platform.action_center import Task +from uipath.platform.common import ( + CreateEscalation, + CreateTask, + InvokeProcess, + WaitEscalation, + WaitJob, + WaitTask, +) + def _try_convert_to_json_format(value: str | None) -> str | None: """Attempts to parse a string as JSON and returns the parsed object or original string. From 07e0fe1637a1dfc759f05521f221b9ba5e8b635e Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Tue, 2 Dec 2025 08:56:44 +0200 Subject: [PATCH 4/4] feat: add folder_path as welll --- src/uipath/_utils/_bindings.py | 6 ++--- .../platform/resume_triggers/_protocol.py | 24 ++++++++----------- tests/sdk/test_bindings.py | 10 ++++---- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/uipath/_utils/_bindings.py b/src/uipath/_utils/_bindings.py index eb10d9e2c..9a6b39360 100644 --- a/src/uipath/_utils/_bindings.py +++ b/src/uipath/_utils/_bindings.py @@ -238,7 +238,7 @@ def resolve_folder_from_bindings( if matched is None: return None, None - if isinstance(matched, ConnectionResourceOverwrite): - return None, matched.folder_identifier + if hasattr(matched, "folder_path"): + return matched.folder_path, matched.folder_identifier - return matched.folder_identifier, None + return None, matched.folder_identifier diff --git a/src/uipath/platform/resume_triggers/_protocol.py b/src/uipath/platform/resume_triggers/_protocol.py index f65240607..c74de2933 100644 --- a/src/uipath/platform/resume_triggers/_protocol.py +++ b/src/uipath/platform/resume_triggers/_protocol.py @@ -261,21 +261,19 @@ async def _handle_task_trigger( resume_trigger: The resume trigger to populate uipath: The UiPath client instance """ - resume_trigger.folder_path = value.app_folder_path - resume_trigger.folder_key = value.app_folder_key - if isinstance(value, (WaitTask, WaitEscalation)): resume_trigger.item_key = value.action.key + resume_trigger.folder_path = value.app_folder_path + resume_trigger.folder_key = value.app_folder_key elif isinstance(value, (CreateTask, CreateEscalation)): resolved_path, resolved_key = resolve_folder_from_bindings( resource_type="app", resource_name=value.app_name, folder_path=value.app_folder_path, ) - if resolved_path: - resume_trigger.folder_path = resolved_path - if resolved_key: - resume_trigger.folder_key = resolved_key + + resume_trigger.folder_path = resolved_path or value.app_folder_path + resume_trigger.folder_key = resolved_key or value.app_folder_key action = await uipath.tasks.create_async( title=value.title, @@ -300,21 +298,19 @@ async def _handle_job_trigger( resume_trigger: The resume trigger to populate uipath: The UiPath client instance """ - resume_trigger.folder_path = value.process_folder_path - resume_trigger.folder_key = value.process_folder_key - if isinstance(value, WaitJob): resume_trigger.item_key = value.job.key + resume_trigger.folder_path = value.process_folder_path + resume_trigger.folder_key = value.process_folder_key elif isinstance(value, InvokeProcess): resolved_path, resolved_key = resolve_folder_from_bindings( resource_type="process", resource_name=value.name, folder_path=value.process_folder_path, ) - if resolved_path: - resume_trigger.folder_path = resolved_path - if resolved_key: - resume_trigger.folder_key = resolved_key + + resume_trigger.folder_path = resolved_path or value.process_folder_path + resume_trigger.folder_key = resolved_key or value.process_folder_key job = await uipath.processes.invoke_async( name=value.name, diff --git a/tests/sdk/test_bindings.py b/tests/sdk/test_bindings.py index 29503097d..95f71e432 100644 --- a/tests/sdk/test_bindings.py +++ b/tests/sdk/test_bindings.py @@ -385,7 +385,7 @@ def test_returns_folder_path_for_generic_overwrite(self): token = _resource_overwrites.set(overwrites) try: result = resolve_folder_from_bindings("app", "my_app") - assert result == ("new_folder", None) + assert result == ("new_folder", "new_folder") finally: _resource_overwrites.reset(token) @@ -433,7 +433,7 @@ def test_prefers_specific_key_with_folder_path(self): token = _resource_overwrites.set(overwrites) try: result = resolve_folder_from_bindings("app", "my_app", "specific_folder") - assert result == ("specific_folder", None) + assert result == ("specific_folder", "specific_folder") finally: _resource_overwrites.reset(token) @@ -447,7 +447,7 @@ def test_falls_back_to_generic_key_when_specific_not_found(self): token = _resource_overwrites.set(overwrites) try: result = resolve_folder_from_bindings("app", "my_app", "unknown_folder") - assert result == ("generic_folder", None) + assert result == ("generic_folder", "generic_folder") finally: _resource_overwrites.reset(token) @@ -463,7 +463,7 @@ def test_works_with_process_resource_type(self): token = _resource_overwrites.set(overwrites) try: result = resolve_folder_from_bindings("process", "my_process") - assert result == ("process_folder", None) + assert result == ("process_folder", "process_folder") finally: _resource_overwrites.reset(token) @@ -482,7 +482,7 @@ async def get_overwrites(): async with ResourceOverwritesContext(get_overwrites): result = resolve_folder_from_bindings("app", "my_app") - assert result == ("resolved_folder", None) + assert result == ("resolved_folder", "resolved_folder") # Context should be cleaned up result_after = resolve_folder_from_bindings("app", "my_app")