diff --git a/src/uipath/_services/_folder_helpers.py b/src/uipath/_services/_folder_helpers.py new file mode 100644 index 000000000..2dffe7615 --- /dev/null +++ b/src/uipath/_services/_folder_helpers.py @@ -0,0 +1,79 @@ +from typing import Optional + +from .folder_service import FolderService + + +def resolve_folder_key( + folder_key: Optional[str], + folder_path: Optional[str], + folders_service: FolderService, + context_folder_key: Optional[str] = None, + context_folder_path: Optional[str] = None, +) -> str: + """Resolve folder key from provided parameters or instance context. + + Args: + folder_key: Optional folder key to use directly + folder_path: Optional folder path to resolve to a key + folders_service: FolderService instance to resolve folder paths + context_folder_key: Optional instance-level folder key fallback + context_folder_path: Optional instance-level folder path fallback + + Returns: + The resolved folder key + + Raises: + ValueError: If folder key cannot be resolved + """ + if folder_key is not None: + return folder_key + + if folder_path is not None: + return folders_service.retrieve_folder_key(folder_path=folder_path) + + if context_folder_key is not None: + return context_folder_key + + if context_folder_path is not None: + return folders_service.retrieve_folder_key(folder_path=context_folder_path) + + raise ValueError("Failed to resolve folder key") + + +async def resolve_folder_key_async( + folder_key: Optional[str], + folder_path: Optional[str], + folders_service: FolderService, + context_folder_key: Optional[str] = None, + context_folder_path: Optional[str] = None, +) -> str: + """Asynchronously resolve folder key from provided parameters or instance context. + + Args: + folder_key: Optional folder key to use directly + folder_path: Optional folder path to resolve to a key + folders_service: FolderService instance to resolve folder paths + context_folder_key: Optional instance-level folder key fallback + context_folder_path: Optional instance-level folder path fallback + + Returns: + The resolved folder key + + Raises: + ValueError: If folder key cannot be resolved + """ + if folder_key is not None: + return folder_key + + if folder_path is not None: + return await folders_service.retrieve_folder_key_async(folder_path=folder_path) + + if context_folder_key is not None: + return context_folder_key + + if context_folder_path is not None: + return await folders_service.retrieve_folder_key_async( + folder_path=context_folder_path + ) + + raise ValueError("Failed to resolve folder key") diff --git a/src/uipath/_services/connections_service.py b/src/uipath/_services/connections_service.py index 9b9ae933a..6bcac45fe 100644 --- a/src/uipath/_services/connections_service.py +++ b/src/uipath/_services/connections_service.py @@ -7,17 +7,19 @@ from .._config import Config from .._execution_context import ExecutionContext +from .._folder_context import FolderContext from .._utils import Endpoint, RequestSpec, header_folder, resource_override from ..models import Connection, ConnectionMetadata, ConnectionToken, EventArguments from ..models.connections import ActivityMetadata, ConnectionTokenType from ..tracing import traced from ._base_service import BaseService +from ._folder_helpers import resolve_folder_key, resolve_folder_key_async from .folder_service import FolderService logger: logging.Logger = logging.getLogger("uipath") -class ConnectionsService(BaseService): +class ConnectionsService(FolderContext, BaseService): """Service for managing UiPath external service connections. This service provides methods to retrieve direct connection information retrieval @@ -177,7 +179,13 @@ def list( """ spec = self._list_spec( name=name, - folder_key=self._folders_service.retrieve_folder_key(folder_path), + folder_key=resolve_folder_key( + folder_key, + folder_path, + self._folders_service, + self._folder_key, + self._folder_path, + ), connector_key=connector_key, skip=skip, top=top, @@ -231,8 +239,12 @@ async def list_async( """ spec = self._list_spec( name=name, - folder_key=await self._folders_service.retrieve_folder_key_async( - folder_path + folder_key=await resolve_folder_key_async( + folder_key, + folder_path, + self._folders_service, + self._folder_key, + self._folder_path, ), connector_key=connector_key, skip=skip, diff --git a/src/uipath/_services/context_grounding_service.py b/src/uipath/_services/context_grounding_service.py index a07cb7b9d..31b282659 100644 --- a/src/uipath/_services/context_grounding_service.py +++ b/src/uipath/_services/context_grounding_service.py @@ -7,7 +7,12 @@ from .._config import Config from .._execution_context import ExecutionContext from .._folder_context import FolderContext -from .._utils import Endpoint, RequestSpec, header_folder, resource_override +from .._utils import ( + Endpoint, + RequestSpec, + header_folder, + resource_override, +) from .._utils.constants import ( LLMV4_REQUEST, ORCHESTRATOR_STORAGE_BUCKET_DATA_SOURCE, @@ -33,6 +38,7 @@ from ..models.exceptions import UnsupportedDataSourceException from ..tracing import traced from ._base_service import BaseService +from ._folder_helpers import resolve_folder_key from .buckets_service import BucketsService from .folder_service import FolderService @@ -672,7 +678,13 @@ def _ingest_spec( folder_key: Optional[str] = None, folder_path: Optional[str] = None, ) -> RequestSpec: - folder_key = self._resolve_folder_key(folder_key, folder_path) + folder_key = resolve_folder_key( + folder_key, + folder_path, + self._folders_service, + self._folder_key, + self._folder_path, + ) return RequestSpec( method="POST", @@ -688,7 +700,13 @@ def _retrieve_spec( folder_key: Optional[str] = None, folder_path: Optional[str] = None, ) -> RequestSpec: - folder_key = self._resolve_folder_key(folder_key, folder_path) + folder_key = resolve_folder_key( + folder_key, + folder_path, + self._folders_service, + self._folder_key, + self._folder_path, + ) return RequestSpec( method="GET", @@ -726,7 +744,13 @@ def _create_spec( Returns: RequestSpec for the create index request """ - folder_key = self._resolve_folder_key(folder_key, folder_path) + folder_key = resolve_folder_key( + folder_key, + folder_path, + self._folders_service, + self._folder_key, + self._folder_path, + ) data_source_dict = self._build_data_source(source) @@ -828,7 +852,13 @@ def _retrieve_by_id_spec( folder_key: Optional[str] = None, folder_path: Optional[str] = None, ) -> RequestSpec: - folder_key = self._resolve_folder_key(folder_key, folder_path) + folder_key = resolve_folder_key( + folder_key, + folder_path, + self._folders_service, + self._folder_key, + self._folder_path, + ) return RequestSpec( method="GET", @@ -844,7 +874,13 @@ def _delete_by_id_spec( folder_key: Optional[str] = None, folder_path: Optional[str] = None, ) -> RequestSpec: - folder_key = self._resolve_folder_key(folder_key, folder_path) + folder_key = resolve_folder_key( + folder_key, + folder_path, + self._folders_service, + self._folder_key, + self._folder_path, + ) return RequestSpec( method="DELETE", @@ -862,7 +898,13 @@ def _search_spec( folder_key: Optional[str] = None, folder_path: Optional[str] = None, ) -> RequestSpec: - folder_key = self._resolve_folder_key(folder_key, folder_path) + folder_key = resolve_folder_key( + folder_key, + folder_path, + self._folders_service, + self._folder_key, + self._folder_path, + ) return RequestSpec( method="POST", @@ -876,22 +918,6 @@ def _search_spec( }, ) - def _resolve_folder_key(self, folder_key, folder_path): - if folder_key is None and folder_path is not None: - folder_key = self._folders_service.retrieve_key(folder_path=folder_path) - - if folder_key is None and folder_path is None: - folder_key = self._folder_key or ( - self._folders_service.retrieve_key(folder_path=self._folder_path) - if self._folder_path - else None - ) - - if folder_key is None: - raise ValueError("ContextGrounding: Failed to resolve folder key") - - return folder_key - def _extract_bucket_info(self, index: ContextGroundingIndex) -> Tuple[str, str]: """Extract bucket information from the index, validating it's a storage bucket data source. diff --git a/src/uipath/_services/folder_service.py b/src/uipath/_services/folder_service.py index ba01aae6d..14bbce7ff 100644 --- a/src/uipath/_services/folder_service.py +++ b/src/uipath/_services/folder_service.py @@ -21,7 +21,7 @@ class FolderService(BaseService): def __init__(self, config: Config, execution_context: ExecutionContext) -> None: super().__init__(config=config, execution_context=execution_context) - def retrieve_folder_key(self, folder_path: str | None) -> str | None: + def retrieve_folder_key(self, folder_path: str | None) -> str: """Resolve a folder path to its corresponding folder key. Args: @@ -41,7 +41,7 @@ def retrieve_folder_key(self, folder_path: str | None) -> str | None: raise ValueError(f"Folder with path '{folder_path}' not found") return resolved_folder_key - async def retrieve_folder_key_async(self, folder_path: str | None) -> str | None: + async def retrieve_folder_key_async(self, folder_path: str | None) -> str: """Asynchronously resolve a folder path to its corresponding folder key. Args: diff --git a/src/uipath/_services/resource_catalog_service.py b/src/uipath/_services/resource_catalog_service.py index 418982cad..bbd204c1a 100644 --- a/src/uipath/_services/resource_catalog_service.py +++ b/src/uipath/_services/resource_catalog_service.py @@ -5,6 +5,10 @@ from uipath._folder_context import FolderContext from uipath._services import FolderService from uipath._services._base_service import BaseService +from uipath._services._folder_helpers import ( + resolve_folder_key, + resolve_folder_key_async, +) from uipath._utils import Endpoint, RequestSpec, header_folder from uipath.models.resource_catalog import Resource, ResourceType from uipath.tracing import traced @@ -217,7 +221,13 @@ def list( if take <= 0: raise ValueError(f"page_size must be greater than 0. Got {page_size}") - resolved_folder_key = self.folder_service.retrieve_folder_key(folder_path) + resolved_folder_key = resolve_folder_key( + folder_key, + folder_path, + self.folder_service, + self._folder_key, + self._folder_path, + ) while True: spec = self._list_spec( @@ -299,8 +309,12 @@ async def list_async( if take <= 0: raise ValueError(f"page_size must be greater than 0. Got {page_size}") - resolved_folder_key = await self.folder_service.retrieve_folder_key_async( - folder_path + resolved_folder_key = await resolve_folder_key_async( + folder_key, + folder_path, + self.folder_service, + self._folder_key, + self._folder_path, ) while True: spec = self._list_spec( @@ -385,7 +399,13 @@ def list_by_type( if take <= 0: raise ValueError(f"page_size must be greater than 0. Got {page_size}") - resolved_folder_key = self.folder_service.retrieve_folder_key(folder_path) + resolved_folder_key = resolve_folder_key( + folder_key, + folder_path, + self.folder_service, + self._folder_key, + self._folder_path, + ) while True: spec = self._list_by_type_spec( @@ -471,8 +491,12 @@ async def list_by_type_async( if take <= 0: raise ValueError(f"page_size must be greater than 0. Got {page_size}") - resolved_folder_key = await self.folder_service.retrieve_folder_key_async( - folder_path + resolved_folder_key = await resolve_folder_key_async( + folder_key, + folder_path, + self.folder_service, + self._folder_key, + self._folder_path, ) while True: diff --git a/tests/sdk/services/test_connections_service.py b/tests/sdk/services/test_connections_service.py index 1f43125d0..d3bf38bc4 100644 --- a/tests/sdk/services/test_connections_service.py +++ b/tests/sdk/services/test_connections_service.py @@ -1,5 +1,5 @@ import json -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from urllib.parse import unquote_plus import pytest @@ -566,28 +566,25 @@ def test_list_with_folder_path_resolution( version: str, ) -> None: """Test list method with folder path resolution.""" - mock_folders_service.retrieve_folder_key.return_value = "folder-123" - - httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/connections_/api/v1/Connections?%24expand=connector%2Cfolder", - status_code=200, - json={"value": []}, - ) - - service.list(folder_path="Finance/Production") + with patch( + "uipath._services.connections_service.resolve_folder_key", + return_value="folder-123", + ): + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/connections_/api/v1/Connections?%24expand=connector%2Cfolder", + status_code=200, + json={"value": []}, + ) - # Verify folder service was called - mock_folders_service.retrieve_folder_key.assert_called_once_with( - "Finance/Production" - ) + service.list(folder_path="Finance/Production") - sent_request = httpx_mock.get_request() - if sent_request is None: - raise Exception("No request was sent") + sent_request = httpx_mock.get_request() + if sent_request is None: + raise Exception("No request was sent") - # Verify the resolved key was used in headers - assert HEADER_FOLDER_KEY in sent_request.headers - assert sent_request.headers[HEADER_FOLDER_KEY] == "folder-123" + # Verify the resolved key was used in headers + assert HEADER_FOLDER_KEY in sent_request.headers + assert sent_request.headers[HEADER_FOLDER_KEY] == "folder-123" def test_list_with_connector_filter( self, diff --git a/tests/sdk/services/test_context_grounding_service.py b/tests/sdk/services/test_context_grounding_service.py index 866b983be..98746da45 100644 --- a/tests/sdk/services/test_context_grounding_service.py +++ b/tests/sdk/services/test_context_grounding_service.py @@ -814,9 +814,10 @@ def test_all_requests_pass_spec_parameters( tenant: str, ) -> None: """Verify that all requests pass spec.method, spec.endpoint, spec.params, and spec.headers correctly.""" - # Mock folder service to always return the test folder key - with patch.object( - service._folders_service, "retrieve_key", return_value="test-folder-key" + # Mock resolve_folder_key to always return the test folder key + with patch( + "uipath._services.context_grounding_service.resolve_folder_key", + return_value="test-folder-key", ): # Test retrieve method with patch.object(service, "request") as mock_request: diff --git a/tests/sdk/services/test_resource_catalog_service.py b/tests/sdk/services/test_resource_catalog_service.py index 4b8b0e0cc..f70cf08bd 100644 --- a/tests/sdk/services/test_resource_catalog_service.py +++ b/tests/sdk/services/test_resource_catalog_service.py @@ -251,13 +251,13 @@ def test_list_resources_without_filters( assert len(resources) == 2 assert resources[0].name == "test-asset" assert resources[1].name == "test-queue" - mock_folder_service.retrieve_folder_key.assert_called_once_with(None) sent_request = httpx_mock.get_request() if sent_request is None: raise Exception("No request was sent") assert sent_request.method == "GET" + assert sent_request.headers["X-UIPATH-FolderKey"] == "test-folder-key" assert str(sent_request.url).endswith("/Entities?skip=0&take=20") assert HEADER_USER_AGENT in sent_request.headers assert ( @@ -297,15 +297,13 @@ def test_list_resources_with_folder_path( assert len(resources) == 1 assert resources[0].name == "finance-asset" - mock_folder_service.retrieve_folder_key.assert_called_once_with( - "/Shared/Finance" - ) sent_request = httpx_mock.get_request() if sent_request is None: raise Exception("No request was sent") assert "X-UIPATH-FolderKey" in sent_request.headers + assert sent_request.headers["X-UIPATH-FolderKey"] == "test-folder-key" def test_list_resources_with_resource_filters( self, @@ -437,13 +435,13 @@ def test_list_by_type_basic( assert resources[0].resource_type == "asset" assert resources[1].name == "number-asset" assert resources[1].resource_type == "asset" - mock_folder_service.retrieve_folder_key.assert_called_once_with(None) sent_request = httpx_mock.get_request() if sent_request is None: raise Exception("No request was sent") assert sent_request.method == "GET" + assert sent_request.headers["X-UIPATH-FolderKey"] == "test-folder-key" assert str(sent_request.url).endswith("/Entities/asset?skip=0&take=20") assert HEADER_USER_AGENT in sent_request.headers assert ( @@ -523,9 +521,7 @@ def test_list_by_type_with_folder_and_subtype( assert len(resources) == 1 assert resources[0].resource_sub_type == "number" - mock_folder_service.retrieve_folder_key.assert_called_once_with( - "/Shared/Finance" - ) + assert resources[0].folder_key == "test-folder-key" sent_request = httpx_mock.get_request() if sent_request is None: @@ -533,6 +529,7 @@ def test_list_by_type_with_folder_and_subtype( assert "entitySubType=number" in str(sent_request.url) assert "X-UIPATH-FolderKey" in sent_request.headers + assert sent_request.headers["X-UIPATH-FolderKey"] == "test-folder-key" def test_list_by_type_pagination( self, @@ -653,7 +650,6 @@ async def test_list_resources_async( assert len(resources) == 1 assert resources[0].name == "async-resource" - mock_folder_service.retrieve_folder_key_async.assert_called_once_with(None) @pytest.mark.asyncio async def test_list_resources_async_with_filters( @@ -690,9 +686,7 @@ async def test_list_resources_async_with_filters( assert len(resources) == 1 assert resources[0].resource_sub_type == "text" - mock_folder_service.retrieve_folder_key_async.assert_called_once_with( - "/Test/Folder" - ) + assert resources[0].folder_key == "test-folder-key" @pytest.mark.asyncio async def test_list_by_type_async_basic( @@ -736,7 +730,6 @@ async def test_list_by_type_async_basic( assert resources[0].resource_type == "asset" assert resources[1].name == "async-asset-2" assert resources[1].resource_type == "asset" - mock_folder_service.retrieve_folder_key_async.assert_called_once_with(None) @pytest.mark.asyncio async def test_list_by_type_async_with_name_filter( @@ -808,9 +801,7 @@ async def test_list_by_type_async_with_folder_and_subtype( assert len(resources) == 1 assert resources[0].resource_sub_type == "transactional" - mock_folder_service.retrieve_folder_key_async.assert_called_once_with( - "/Production" - ) + assert resources[0].folder_key == "test-folder-key" @pytest.mark.asyncio async def test_list_by_type_async_pagination(