From d74f116d2eb467cbd308e61f07c4b4bcf32a73b8 Mon Sep 17 00:00:00 2001 From: Harshit Rohatgi Date: Tue, 7 Apr 2026 15:50:55 +0530 Subject: [PATCH 1/3] Revert "Fix/entity as resource overwrite (#1544)" as the upstream change is not checked in yet This reverts commit 11e86d4c9dce3ae2157daccd6f4f998f0c6f74d5. --- .../src/uipath/platform/_uipath.py | 4 +- .../src/uipath/platform/common/__init__.py | 2 - .../src/uipath/platform/common/_bindings.py | 20 +- .../platform/entities/_entities_service.py | 186 ++---------------- .../tests/services/test_entities_service.py | 185 ++++------------- packages/uipath/pyproject.toml | 2 +- .../src/uipath/_resources/SDK_REFERENCE.md | 4 +- .../tests/resource_overrides/overwrites.json | 4 - .../test_resource_overrides.py | 5 - 9 files changed, 59 insertions(+), 353 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/_uipath.py b/packages/uipath-platform/src/uipath/platform/_uipath.py index e1d60fc39..87c3a17f0 100644 --- a/packages/uipath-platform/src/uipath/platform/_uipath.py +++ b/packages/uipath-platform/src/uipath/platform/_uipath.py @@ -139,9 +139,7 @@ def llm(self) -> UiPathLlmChatService: @property def entities(self) -> EntitiesService: - return EntitiesService( - self._config, self._execution_context, folders_service=self.folders - ) + return EntitiesService(self._config, self._execution_context) @cached_property def resource_catalog(self) -> ResourceCatalogService: diff --git a/packages/uipath-platform/src/uipath/platform/common/__init__.py b/packages/uipath-platform/src/uipath/platform/common/__init__.py index 9070d0d70..40fc1ac34 100644 --- a/packages/uipath-platform/src/uipath/platform/common/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/common/__init__.py @@ -7,7 +7,6 @@ from ._base_service import BaseService from ._bindings import ( ConnectionResourceOverwrite, - EntityResourceOverwrite, GenericResourceOverwrite, ResourceOverwrite, ResourceOverwriteParser, @@ -101,7 +100,6 @@ "EndpointManager", "jsonschema_to_pydantic", "ConnectionResourceOverwrite", - "EntityResourceOverwrite", "GenericResourceOverwrite", "ResourceOverwrite", "ResourceOverwriteParser", diff --git a/packages/uipath-platform/src/uipath/platform/common/_bindings.py b/packages/uipath-platform/src/uipath/platform/common/_bindings.py index 1ccb2b1fc..449d2a7ef 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_bindings.py +++ b/packages/uipath-platform/src/uipath/platform/common/_bindings.py @@ -45,7 +45,7 @@ def folder_identifier(self) -> str: class GenericResourceOverwrite(ResourceOverwrite): resource_type: Literal[ - "process", "index", "app", "asset", "bucket", "mcpServer", "queue" + "process", "index", "app", "asset", "bucket", "mcpServer", "queue", "entity" ] name: str = Field(alias="name") folder_path: str = Field(alias="folderPath") @@ -59,20 +59,6 @@ def folder_identifier(self) -> str: return self.folder_path -class EntityResourceOverwrite(ResourceOverwrite): - resource_type: Literal["entity"] - name: str = Field(alias="name") - folder_key: str = Field(alias="folderId") - - @property - def resource_identifier(self) -> str: - return self.name - - @property - def folder_identifier(self) -> str: - return self.folder_key - - class ConnectionResourceOverwrite(ResourceOverwrite): resource_type: Literal["connection"] # In eval context, studio web provides "ConnectionId". @@ -97,9 +83,7 @@ def folder_identifier(self) -> str: ResourceOverwriteUnion = Annotated[ - Union[ - GenericResourceOverwrite, EntityResourceOverwrite, ConnectionResourceOverwrite - ], + Union[GenericResourceOverwrite, ConnectionResourceOverwrite], Field(discriminator="resource_type"), ] diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py index dc08131c5..f30c9492e 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py +++ b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py @@ -1,4 +1,3 @@ -import logging from typing import Any, Dict, List, Optional, Type import sqlparse @@ -8,21 +7,16 @@ from uipath.core.tracing import traced from ..common._base_service import BaseService -from ..common._bindings import EntityResourceOverwrite, _resource_overwrites from ..common._config import UiPathApiConfig from ..common._execution_context import UiPathExecutionContext from ..common._models import Endpoint, RequestSpec -from ..orchestrator._folder_service import FolderService from .entities import ( Entity, EntityRecord, EntityRecordsBatchResponse, - EntityRouting, QueryRoutingOverrideContext, ) -logger = logging.getLogger(__name__) - _FORBIDDEN_DML = {"INSERT", "UPDATE", "DELETE", "MERGE", "REPLACE"} _FORBIDDEN_DDL = {"DROP", "ALTER", "CREATE", "TRUNCATE"} _DISALLOWED_KEYWORDS = [ @@ -53,32 +47,9 @@ class EntitiesService(BaseService): """ def __init__( - self, - config: UiPathApiConfig, - execution_context: UiPathExecutionContext, - folders_service: Optional[FolderService] = None, - folders_map: Optional[Dict[str, str]] = None, + self, config: UiPathApiConfig, execution_context: UiPathExecutionContext ) -> None: super().__init__(config=config, execution_context=execution_context) - self._folders_service = folders_service - self._folders_map = folders_map or {} - - def with_folders_map(self, folders_map: Dict[str, str]) -> "EntitiesService": - """Return a new EntitiesService configured with the given folders map. - - The map is used to build a routing context automatically when - ``query_entity_records`` is called without an explicit routing context. - Folder paths in the map are resolved to folder keys via ``FolderService``. - - Args: - folders_map: Mapping of entity name to folder path. - """ - return EntitiesService( - config=self._config, - execution_context=self._execution_context, - folders_service=self._folders_service, - folders_map=folders_map, - ) @traced(name="entity_retrieve", run_type="uipath") def retrieve(self, entity_key: str) -> Entity: @@ -446,6 +417,7 @@ class CustomerRecord: def query_entity_records( self, sql_query: str, + routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: """Query entity records using a validated SQL query. @@ -455,10 +427,9 @@ def query_entity_records( sql_query (str): A SQL SELECT query to execute against Data Service entities. Only SELECT statements are allowed. Queries without WHERE must include a LIMIT clause. Subqueries and multi-statement queries are not permitted. - - Notes: - A routing context is always derived from the configured ``folders_map`` - when present and included in the request body. + routing_context (Optional[QueryRoutingOverrideContext]): Per-entity routing context + for multi-folder queries. When present, included in the request body + and takes precedence over the folder header on the backend. Returns: List[Dict[str, Any]]: A list of result records as dictionaries. @@ -467,12 +438,15 @@ def query_entity_records( ValueError: If the SQL query fails validation (e.g., non-SELECT, missing WHERE/LIMIT, forbidden keywords, subqueries). """ - return self._query_entities_for_records(sql_query) + return self._query_entities_for_records( + sql_query, routing_context=routing_context + ) @traced(name="entity_query_records", run_type="uipath") async def query_entity_records_async( self, sql_query: str, + routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: """Asynchronously query entity records using a validated SQL query. @@ -482,10 +456,9 @@ async def query_entity_records_async( sql_query (str): A SQL SELECT query to execute against Data Service entities. Only SELECT statements are allowed. Queries without WHERE must include a LIMIT clause. Subqueries and multi-statement queries are not permitted. - - Notes: - A routing context is always derived from the configured ``folders_map`` - when present and included in the request body. + routing_context (Optional[QueryRoutingOverrideContext]): Per-entity routing context + for multi-folder queries. When present, included in the request body + and takes precedence over the folder header on the backend. Returns: List[Dict[str, Any]]: A list of result records as dictionaries. @@ -494,14 +467,17 @@ async def query_entity_records_async( ValueError: If the SQL query fails validation (e.g., non-SELECT, missing WHERE/LIMIT, forbidden keywords, subqueries). """ - return await self._query_entities_for_records_async(sql_query) + return await self._query_entities_for_records_async( + sql_query, routing_context=routing_context + ) def _query_entities_for_records( self, sql_query: str, + *, + routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: self._validate_sql_query(sql_query) - routing_context = self._build_routing_context_from_map() spec = self._query_entity_records_spec(sql_query, routing_context) response = self.request(spec.method, spec.endpoint, json=spec.json) return response.json().get("results", []) @@ -509,9 +485,10 @@ def _query_entities_for_records( async def _query_entities_for_records_async( self, sql_query: str, + *, + routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: self._validate_sql_query(sql_query) - routing_context = await self._build_routing_context_from_map_async() spec = self._query_entity_records_spec(sql_query, routing_context) response = await self.request_async(spec.method, spec.endpoint, json=spec.json) return response.json().get("results", []) @@ -1015,131 +992,6 @@ def _query_entity_records_spec( json=body, ) - def _build_routing_context_from_map( - self, - ) -> Optional[QueryRoutingOverrideContext]: - """Build a routing context from the configured folders_map and context overwrites. - - Folder paths in the map are resolved to folder keys via FolderService. - Entity overwrites from the active ``ResourceOverwritesContext`` are - merged in, supplying ``override_entity_name`` when the overwrite - provides a different entity name. - - Returns: - A QueryRoutingOverrideContext if routing entries exist, - None otherwise. - """ - resolved = self._resolve_folder_paths_to_ids() - return self._build_routing_context_from_resolved_map(resolved) - - async def _build_routing_context_from_map_async( - self, - ) -> Optional[QueryRoutingOverrideContext]: - """Async version of _build_routing_context_from_map.""" - resolved = await self._resolve_folder_paths_to_ids_async() - return self._build_routing_context_from_resolved_map(resolved) - - def _resolve_folder_paths_to_ids(self) -> Optional[dict[str, str]]: - if not self._folders_map: - return None - - resolved: dict[str, str] = {} - for folder_path in set(self._folders_map.values()): - if self._folders_service is not None: - folder_key = self._folders_service.retrieve_folder_key(folder_path) - if folder_key is not None: - resolved[folder_path] = folder_key - continue - resolved[folder_path] = folder_path - - return resolved - - async def _resolve_folder_paths_to_ids_async(self) -> Optional[dict[str, str]]: - if not self._folders_map: - return None - - resolved: dict[str, str] = {} - for folder_path in set(self._folders_map.values()): - if self._folders_service is not None: - folder_key = await self._folders_service.retrieve_folder_key_async( - folder_path - ) - if folder_key is not None: - resolved[folder_path] = folder_key - continue - resolved[folder_path] = folder_path - - return resolved - - @staticmethod - def _get_entity_overwrites_from_context() -> Dict[str, EntityResourceOverwrite]: - """Extract entity overwrites from the active ResourceOverwritesContext. - - Returns: - A dict mapping original entity name to its EntityResourceOverwrite. - """ - context_overwrites = _resource_overwrites.get() - if not context_overwrites: - return {} - - result: Dict[str, EntityResourceOverwrite] = {} - for key, overwrite in context_overwrites.items(): - if isinstance(overwrite, EntityResourceOverwrite): - # Key format is "entity." - original_name = key.split(".", 1)[1] if "." in key else key - result[original_name] = overwrite - return result - - def _build_routing_context_from_resolved_map( - self, - resolved: Optional[dict[str, str]], - ) -> Optional[QueryRoutingOverrideContext]: - entity_overwrites = self._get_entity_overwrites_from_context() - - routings: List[EntityRouting] = [] - - # Add routings from folders_map - if self._folders_map and resolved is not None: - for name, folder_path in self._folders_map.items(): - overwrite = entity_overwrites.pop(name, None) - override_name = ( - overwrite.resource_identifier - if overwrite and overwrite.resource_identifier != name - else None - ) - folder_id = ( - overwrite.folder_identifier - if overwrite - else resolved.get(folder_path, folder_path) - ) - routings.append( - EntityRouting( - entity_name=name, - folder_id=folder_id, - override_entity_name=override_name, - ) - ) - - # Add routings from context overwrites not already in folders_map - for original_name, overwrite in entity_overwrites.items(): - override_name = ( - overwrite.resource_identifier - if overwrite.resource_identifier != original_name - else None - ) - routings.append( - EntityRouting( - entity_name=original_name, - folder_id=overwrite.folder_identifier, - override_entity_name=override_name, - ) - ) - - if not routings: - return None - - return QueryRoutingOverrideContext(entity_routings=routings) - def _insert_batch_spec(self, entity_key: str, records: List[Any]) -> RequestSpec: return RequestSpec( method="POST", diff --git a/packages/uipath-platform/tests/services/test_entities_service.py b/packages/uipath-platform/tests/services/test_entities_service.py index 4993a1367..8a9abafef 100644 --- a/packages/uipath-platform/tests/services/test_entities_service.py +++ b/packages/uipath-platform/tests/services/test_entities_service.py @@ -8,7 +8,7 @@ from pytest_httpx import HTTPXMock from uipath.platform import UiPathApiConfig, UiPathExecutionContext -from uipath.platform.entities import Entity +from uipath.platform.entities import Entity, EntityRouting, QueryRoutingOverrideContext from uipath.platform.entities._entities_service import EntitiesService @@ -390,21 +390,28 @@ async def test_query_entity_records_async_calls_request_for_valid_sql( assert result == [{"id": "c1"}] service.request_async.assert_called_once() - def test_query_entity_records_builds_routing_context_from_folders_map( + def test_query_entity_records_with_routing_context( self, - config: UiPathApiConfig, - execution_context: UiPathExecutionContext, + service: EntitiesService, ) -> None: - service = EntitiesService( - config=config, - execution_context=execution_context, - folders_map={"Customers": "solution_folder", "Orders": "folder-2"}, - ) response = MagicMock() response.json.return_value = {"results": [{"id": 1}]} service.request = MagicMock(return_value=response) # type: ignore[method-assign] - result = service.query_entity_records("SELECT id FROM Customers LIMIT 10") + routing = QueryRoutingOverrideContext( + entity_routings=[ + EntityRouting(entity_name="Customers", folder_id="folder-1"), + EntityRouting( + entity_name="Orders", + folder_id="folder-2", + override_entity_name="OrdersV2", + ), + ] + ) + + result = service.query_entity_records( + "SELECT id FROM Customers LIMIT 10", routing_context=routing + ) assert result == [{"id": 1}] call_kwargs = service.request.call_args @@ -412,38 +419,39 @@ def test_query_entity_records_builds_routing_context_from_folders_map( assert body["query"] == "SELECT id FROM Customers LIMIT 10" assert body["routingContext"] == { "entityRoutings": [ - {"entityName": "Customers", "folderId": "solution_folder"}, - {"entityName": "Orders", "folderId": "folder-2"}, + {"entityName": "Customers", "folderId": "folder-1"}, + { + "entityName": "Orders", + "folderId": "folder-2", + "overrideEntityName": "OrdersV2", + }, ] } @pytest.mark.anyio - async def test_query_entity_records_async_builds_routing_context_from_folders_map( + async def test_query_entity_records_async_with_routing_context( self, - config: UiPathApiConfig, - execution_context: UiPathExecutionContext, + service: EntitiesService, ) -> None: - service = EntitiesService( - config=config, - execution_context=execution_context, - folders_map={"Customers": "solution_folder"}, - ) response = MagicMock() response.json.return_value = {"results": [{"id": "c1"}]} service.request_async = AsyncMock(return_value=response) # type: ignore[method-assign] + routing = QueryRoutingOverrideContext( + entity_routings=[ + EntityRouting(entity_name="Customers", folder_id="folder-1"), + ] + ) + result = await service.query_entity_records_async( - "SELECT id FROM Customers WHERE id = 'c1'" + "SELECT id FROM Customers WHERE id = 'c1'", + routing_context=routing, ) assert result == [{"id": "c1"}] call_kwargs = service.request_async.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") - assert body["routingContext"] == { - "entityRoutings": [ - {"entityName": "Customers", "folderId": "solution_folder"}, - ] - } + assert "routingContext" in body def test_query_entity_records_without_routing_context_omits_key( self, @@ -458,128 +466,3 @@ def test_query_entity_records_without_routing_context_omits_key( call_kwargs = service.request.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") assert "routingContext" not in body - - def test_query_entity_records_picks_up_entity_overwrites_from_context( - self, - config: UiPathApiConfig, - execution_context: UiPathExecutionContext, - ) -> None: - from uipath.platform.common._bindings import ( - EntityResourceOverwrite, - _resource_overwrites, - ) - - service = EntitiesService( - config=config, - execution_context=execution_context, - ) - response = MagicMock() - response.json.return_value = {"results": [{"id": 1}]} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] - - overwrite = EntityResourceOverwrite( - resource_type="entity", - name="Overwritten Customers", - folder_key="overwritten-folder-id", - ) - token = _resource_overwrites.set({"entity.Customers": overwrite}) - try: - service.query_entity_records("SELECT id FROM Customers LIMIT 10") - finally: - _resource_overwrites.reset(token) - - call_kwargs = service.request.call_args - body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") - assert body["routingContext"] == { - "entityRoutings": [ - { - "entityName": "Customers", - "folderId": "overwritten-folder-id", - "overrideEntityName": "Overwritten Customers", - }, - ] - } - - def test_query_entity_records_merges_folders_map_with_context_overwrites( - self, - config: UiPathApiConfig, - execution_context: UiPathExecutionContext, - ) -> None: - from uipath.platform.common._bindings import ( - EntityResourceOverwrite, - _resource_overwrites, - ) - - service = EntitiesService( - config=config, - execution_context=execution_context, - folders_map={"Customers": "original-folder", "Orders": "orders-folder"}, - ) - response = MagicMock() - response.json.return_value = {"results": []} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] - - # Overwrite only Customers — Orders should keep its folders_map value - overwrite = EntityResourceOverwrite( - resource_type="entity", - name="Overwritten Customers", - folder_key="overwritten-folder-id", - ) - token = _resource_overwrites.set({"entity.Customers": overwrite}) - try: - service.query_entity_records("SELECT id FROM Customers LIMIT 10") - finally: - _resource_overwrites.reset(token) - - call_kwargs = service.request.call_args - body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") - routings = body["routingContext"]["entityRoutings"] - # Customers overwritten by context - assert { - "entityName": "Customers", - "folderId": "overwritten-folder-id", - "overrideEntityName": "Overwritten Customers", - } in routings - # Orders unchanged from folders_map - assert {"entityName": "Orders", "folderId": "orders-folder"} in routings - - def test_query_entity_records_context_overwrite_same_name_no_override_field( - self, - config: UiPathApiConfig, - execution_context: UiPathExecutionContext, - ) -> None: - from uipath.platform.common._bindings import ( - EntityResourceOverwrite, - _resource_overwrites, - ) - - service = EntitiesService( - config=config, - execution_context=execution_context, - ) - response = MagicMock() - response.json.return_value = {"results": []} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] - - # Same entity name — only folder changes, no override_entity_name needed - overwrite = EntityResourceOverwrite( - resource_type="entity", - name="Customers", - folder_key="different-folder-id", - ) - token = _resource_overwrites.set({"entity.Customers": overwrite}) - try: - service.query_entity_records("SELECT id FROM Customers LIMIT 10") - finally: - _resource_overwrites.reset(token) - - call_kwargs = service.request.call_args - body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") - assert body["routingContext"] == { - "entityRoutings": [ - { - "entityName": "Customers", - "folderId": "different-folder-id", - }, - ] - } diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 58f523a02..a96ce081a 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.42" +version = "2.10.41" 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/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md index 5b03700be..4af1b60ae 100644 --- a/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md +++ b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md @@ -500,10 +500,10 @@ sdk.entities.list_records(entity_key: str, schema: Optional[Type[Any]]=None, sta sdk.entities.list_records_async(entity_key: str, schema: Optional[Type[Any]]=None, start: Optional[int]=None, limit: Optional[int]=None) -> typing.List[uipath.platform.entities.entities.EntityRecord] # Query entity records using a validated SQL query. -sdk.entities.query_entity_records(sql_query: str) -> typing.List[typing.Dict[str, typing.Any]] +sdk.entities.query_entity_records(sql_query: str, routing_context: Optional[uipath.platform.entities.entities.QueryRoutingOverrideContext]=None) -> typing.List[typing.Dict[str, typing.Any]] # Asynchronously query entity records using a validated SQL query. -sdk.entities.query_entity_records_async(sql_query: str) -> typing.List[typing.Dict[str, typing.Any]] +sdk.entities.query_entity_records_async(sql_query: str, routing_context: Optional[uipath.platform.entities.entities.QueryRoutingOverrideContext]=None) -> typing.List[typing.Dict[str, typing.Any]] # Retrieve an entity by its key. sdk.entities.retrieve(entity_key: str) -> uipath.platform.entities.entities.Entity diff --git a/packages/uipath/tests/resource_overrides/overwrites.json b/packages/uipath/tests/resource_overrides/overwrites.json index e0bca84ba..c58744a69 100644 --- a/packages/uipath/tests/resource_overrides/overwrites.json +++ b/packages/uipath/tests/resource_overrides/overwrites.json @@ -28,9 +28,5 @@ "mcpServer.mcp_server_name": { "name": "Overwritten MCP Server Name", "folderPath": "Overwritten/MCPServer/Folder" - }, - "entity.entity_name": { - "name": "Overwritten Entity Name", - "folderId": "overwritten-entity-folder-id-123" } } \ No newline at end of file diff --git a/packages/uipath/tests/resource_overrides/test_resource_overrides.py b/packages/uipath/tests/resource_overrides/test_resource_overrides.py index 8d39a762d..c15bc113b 100644 --- a/packages/uipath/tests/resource_overrides/test_resource_overrides.py +++ b/packages/uipath/tests/resource_overrides/test_resource_overrides.py @@ -310,11 +310,6 @@ def test_parse_overwrites_with_type_adapter(self, overwrites_data): assert mcp_server.resource_identifier == "Overwritten MCP Server Name" assert mcp_server.folder_identifier == "Overwritten/MCPServer/Folder" - # Verify entity overwrite - entity = parsed_overwrites["entity.entity_name"] - assert entity.resource_identifier == "Overwritten Entity Name" - assert entity.folder_identifier == "overwritten-entity-folder-id-123" - def test_overrides_decorator_should_pop_kwargs_dict_when_present(self): from uipath.platform.common import resource_override From d0fdafd7d574c7284d7aba46fd4da09c8a1b2689 Mon Sep 17 00:00:00 2001 From: Harshit Rohatgi Date: Wed, 8 Apr 2026 13:29:20 +0530 Subject: [PATCH 2/3] fix(platform): support entity resource overwrites with folder resolution --- .../src/uipath/platform/_uipath.py | 4 +- .../src/uipath/platform/common/__init__.py | 2 + .../src/uipath/platform/common/_bindings.py | 54 ++- .../src/uipath/platform/entities/__init__.py | 4 + .../platform/entities/_entities_service.py | 354 +++++++++++++++- .../platform/entities/_entity_resolution.py | 266 ++++++++++++ .../src/uipath/platform/entities/entities.py | 40 +- .../tests/services/test_entities_service.py | 377 ++++++++++++++++-- .../src/uipath/_resources/SDK_REFERENCE.md | 19 +- .../uipath/src/uipath/agent/models/agent.py | 11 +- .../uipath/tests/agent/models/test_agent.py | 6 +- .../tests/resource_overrides/overwrites.json | 4 + .../test_resource_overrides.py | 5 + 13 files changed, 1073 insertions(+), 73 deletions(-) create mode 100644 packages/uipath-platform/src/uipath/platform/entities/_entity_resolution.py diff --git a/packages/uipath-platform/src/uipath/platform/_uipath.py b/packages/uipath-platform/src/uipath/platform/_uipath.py index 87c3a17f0..e1d60fc39 100644 --- a/packages/uipath-platform/src/uipath/platform/_uipath.py +++ b/packages/uipath-platform/src/uipath/platform/_uipath.py @@ -139,7 +139,9 @@ def llm(self) -> UiPathLlmChatService: @property def entities(self) -> EntitiesService: - return EntitiesService(self._config, self._execution_context) + return EntitiesService( + self._config, self._execution_context, folders_service=self.folders + ) @cached_property def resource_catalog(self) -> ResourceCatalogService: diff --git a/packages/uipath-platform/src/uipath/platform/common/__init__.py b/packages/uipath-platform/src/uipath/platform/common/__init__.py index 40fc1ac34..9070d0d70 100644 --- a/packages/uipath-platform/src/uipath/platform/common/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/common/__init__.py @@ -7,6 +7,7 @@ from ._base_service import BaseService from ._bindings import ( ConnectionResourceOverwrite, + EntityResourceOverwrite, GenericResourceOverwrite, ResourceOverwrite, ResourceOverwriteParser, @@ -100,6 +101,7 @@ "EndpointManager", "jsonschema_to_pydantic", "ConnectionResourceOverwrite", + "EntityResourceOverwrite", "GenericResourceOverwrite", "ResourceOverwrite", "ResourceOverwriteParser", diff --git a/packages/uipath-platform/src/uipath/platform/common/_bindings.py b/packages/uipath-platform/src/uipath/platform/common/_bindings.py index 449d2a7ef..321d05694 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_bindings.py +++ b/packages/uipath-platform/src/uipath/platform/common/_bindings.py @@ -14,7 +14,14 @@ Union, ) -from pydantic import AliasChoices, BaseModel, ConfigDict, Field, TypeAdapter +from pydantic import ( + AliasChoices, + BaseModel, + ConfigDict, + Field, + TypeAdapter, + model_validator, +) logger = logging.getLogger(__name__) @@ -45,7 +52,7 @@ def folder_identifier(self) -> str: class GenericResourceOverwrite(ResourceOverwrite): resource_type: Literal[ - "process", "index", "app", "asset", "bucket", "mcpServer", "queue", "entity" + "process", "index", "app", "asset", "bucket", "mcpServer", "queue" ] name: str = Field(alias="name") folder_path: str = Field(alias="folderPath") @@ -59,6 +66,29 @@ def folder_identifier(self) -> str: return self.folder_path +class EntityResourceOverwrite(ResourceOverwrite): + resource_type: Literal["entity"] + name: str = Field(alias="name") + folder_id: Optional[str] = Field(default=None, alias="folderId") + folder_path: Optional[str] = Field(default=None, alias="folderPath") + + @model_validator(mode="after") + def validate_folder_identifier(self) -> "EntityResourceOverwrite": + if self.folder_id and self.folder_path: + raise ValueError("Only one of folderId or folderPath may be provided.") + if not self.folder_id and not self.folder_path: + raise ValueError("Either folderId or folderPath must be provided.") + return self + + @property + def resource_identifier(self) -> str: + return self.name + + @property + def folder_identifier(self) -> str: + return self.folder_id or self.folder_path or "" + + class ConnectionResourceOverwrite(ResourceOverwrite): resource_type: Literal["connection"] # In eval context, studio web provides "ConnectionId". @@ -83,7 +113,9 @@ def folder_identifier(self) -> str: ResourceOverwriteUnion = Annotated[ - Union[GenericResourceOverwrite, ConnectionResourceOverwrite], + Union[ + GenericResourceOverwrite, EntityResourceOverwrite, ConnectionResourceOverwrite + ], Field(discriminator="resource_type"), ] @@ -112,9 +144,23 @@ def parse(cls, key: str, value: dict[str, Any]) -> ResourceOverwrite: The appropriate ResourceOverwrite subclass instance """ resource_type = key.split(".")[0] - value_with_type = {"resource_type": resource_type, **value} + normalized_value = cls._normalize_value(resource_type, value) + value_with_type = {"resource_type": resource_type, **normalized_value} return cls._adapter.validate_python(value_with_type) + @staticmethod + def _normalize_value(resource_type: str, value: dict[str, Any]) -> dict[str, Any]: + if resource_type != "entity": + return value + + normalized = dict(value) + if "folderId" in normalized: + normalized["folder_id"] = normalized["folderId"] + if "folderPath" in normalized: + normalized["folder_path"] = normalized["folderPath"] + + return normalized + _resource_overwrites: ContextVar[Optional[dict[str, ResourceOverwrite]]] = ContextVar( "resource_overwrites", default=None diff --git a/packages/uipath-platform/src/uipath/platform/entities/__init__.py b/packages/uipath-platform/src/uipath/platform/entities/__init__.py index bbc43cdb7..6c9ac7f9f 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/entities/__init__.py @@ -5,12 +5,14 @@ from ._entities_service import EntitiesService from .entities import ( + DataFabricEntityItem, Entity, EntityField, EntityFieldMetadata, EntityRecord, EntityRecordsBatchResponse, EntityRouting, + EntitySetResolution, ExternalField, ExternalObject, ExternalSourceFields, @@ -22,12 +24,14 @@ ) __all__ = [ + "DataFabricEntityItem", "EntitiesService", "Entity", "EntityField", "EntityRecord", "EntityFieldMetadata", "EntityRouting", + "EntitySetResolution", "FieldDataType", "FieldMetadata", "EntityRecordsBatchResponse", diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py index f30c9492e..2cd16f62c 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py +++ b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py @@ -1,3 +1,4 @@ +import logging from typing import Any, Dict, List, Optional, Type import sqlparse @@ -7,16 +8,32 @@ from uipath.core.tracing import traced from ..common._base_service import BaseService +from ..common._bindings import EntityResourceOverwrite, _resource_overwrites from ..common._config import UiPathApiConfig from ..common._execution_context import UiPathExecutionContext from ..common._models import Endpoint, RequestSpec +from ..common.constants import HEADER_FOLDER_KEY +from ..orchestrator._folder_service import FolderService +from ._entity_resolution import ( + build_resolution_routing_context, + build_resolution_service, + create_resolution_plan, + create_resolution_plan_async, + fetch_resolved_entities, + fetch_resolved_entities_async, +) from .entities import ( + DataFabricEntityItem, Entity, EntityRecord, EntityRecordsBatchResponse, + EntityRouting, + EntitySetResolution, QueryRoutingOverrideContext, ) +logger = logging.getLogger(__name__) + _FORBIDDEN_DML = {"INSERT", "UPDATE", "DELETE", "MERGE", "REPLACE"} _FORBIDDEN_DDL = {"DROP", "ALTER", "CREATE", "TRUNCATE"} _DISALLOWED_KEYWORDS = [ @@ -47,9 +64,43 @@ class EntitiesService(BaseService): """ def __init__( - self, config: UiPathApiConfig, execution_context: UiPathExecutionContext + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + folders_service: Optional[FolderService] = None, + folders_map: Optional[Dict[str, str]] = None, + entity_name_overrides: Optional[Dict[str, str]] = None, + routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> None: super().__init__(config=config, execution_context=execution_context) + self._folders_service = folders_service + self._folders_map = folders_map or {} + self._effective_entity_names = entity_name_overrides or {} + self._routing_context = routing_context + + def with_folders_map( + self, + folders_map: Dict[str, str], + entity_name_overrides: Optional[Dict[str, str]] = None, + ) -> "EntitiesService": + """Return a new EntitiesService configured with the given folders map. + + The map is used to build a routing context automatically when + ``query_entity_records`` is called without an explicit routing context. + Folder paths in the map are resolved to folder keys via ``FolderService``. + + Args: + folders_map: Mapping of entity name to folder path. + entity_name_overrides: Mapping of original entity name to + overridden entity name. + """ + return EntitiesService( + config=self._config, + execution_context=self._execution_context, + folders_service=self._folders_service, + folders_map=folders_map, + entity_name_overrides=entity_name_overrides, + ) @traced(name="entity_retrieve", run_type="uipath") def retrieve(self, entity_key: str) -> Entity: @@ -128,6 +179,44 @@ async def retrieve_async(self, entity_key: str) -> Entity: return Entity.model_validate(response.json()) + @traced(name="entity_retrieve_by_name", run_type="uipath") + def retrieve_by_name( + self, entity_name: str, folder_key: Optional[str] = None + ) -> Entity: + """Retrieve an entity by its name. + + The server resolves the entity within the folder identified by + ``folder_key``. When omitted the default folder from the + execution context is used. + + Args: + entity_name: The name of the entity. + folder_key: Optional folder key for disambiguation. + """ + spec = self._retrieve_by_name_spec(entity_name) + headers = self._folder_key_headers(folder_key) + response = self.request(spec.method, spec.endpoint, headers=headers) + return Entity.model_validate(response.json()) + + @traced(name="entity_retrieve_by_name", run_type="uipath") + async def retrieve_by_name_async( + self, entity_name: str, folder_key: Optional[str] = None + ) -> Entity: + """Asynchronously retrieve an entity by its name. + + The server resolves the entity within the folder identified by + ``folder_key``. When omitted the default folder from the + execution context is used. + + Args: + entity_name: The name of the entity. + folder_key: Optional folder key for disambiguation. + """ + spec = self._retrieve_by_name_spec(entity_name) + headers = self._folder_key_headers(folder_key) + response = await self.request_async(spec.method, spec.endpoint, headers=headers) + return Entity.model_validate(response.json()) + @traced(name="list_entities", run_type="uipath") def list_entities(self) -> List[Entity]: """List all entities in Data Service. @@ -417,7 +506,6 @@ class CustomerRecord: def query_entity_records( self, sql_query: str, - routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: """Query entity records using a validated SQL query. @@ -427,9 +515,10 @@ def query_entity_records( sql_query (str): A SQL SELECT query to execute against Data Service entities. Only SELECT statements are allowed. Queries without WHERE must include a LIMIT clause. Subqueries and multi-statement queries are not permitted. - routing_context (Optional[QueryRoutingOverrideContext]): Per-entity routing context - for multi-folder queries. When present, included in the request body - and takes precedence over the folder header on the backend. + + Notes: + A routing context is always derived from the configured ``folders_map`` + when present and included in the request body. Returns: List[Dict[str, Any]]: A list of result records as dictionaries. @@ -438,15 +527,12 @@ def query_entity_records( ValueError: If the SQL query fails validation (e.g., non-SELECT, missing WHERE/LIMIT, forbidden keywords, subqueries). """ - return self._query_entities_for_records( - sql_query, routing_context=routing_context - ) + return self._query_entities_for_records(sql_query) @traced(name="entity_query_records", run_type="uipath") async def query_entity_records_async( self, sql_query: str, - routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: """Asynchronously query entity records using a validated SQL query. @@ -456,9 +542,10 @@ async def query_entity_records_async( sql_query (str): A SQL SELECT query to execute against Data Service entities. Only SELECT statements are allowed. Queries without WHERE must include a LIMIT clause. Subqueries and multi-statement queries are not permitted. - routing_context (Optional[QueryRoutingOverrideContext]): Per-entity routing context - for multi-folder queries. When present, included in the request body - and takes precedence over the folder header on the backend. + + Notes: + A routing context is always derived from the configured ``folders_map`` + when present and included in the request body. Returns: List[Dict[str, Any]]: A list of result records as dictionaries. @@ -467,17 +554,82 @@ async def query_entity_records_async( ValueError: If the SQL query fails validation (e.g., non-SELECT, missing WHERE/LIMIT, forbidden keywords, subqueries). """ - return await self._query_entities_for_records_async( - sql_query, routing_context=routing_context + return await self._query_entities_for_records_async(sql_query) + + @traced(name="resolve_entity_set", run_type="uipath") + def resolve_entity_set( + self, + items: list[DataFabricEntityItem], + ) -> EntitySetResolution: + """Resolve an agent entity set, applying resource overwrites.""" + plan = create_resolution_plan( + items, + _resource_overwrites.get() or {}, + lambda folder_path: ( + self._folders_service.retrieve_folder_key(folder_path) + if self._folders_service is not None + else None + ), + ) + entities = fetch_resolved_entities( + plan, + self.retrieve, + self.retrieve_by_name, + logger, + ) + resolution_service: EntitiesService = build_resolution_service( # type: ignore[assignment] + config=self._config, + execution_context=self._execution_context, + folders_service=self._folders_service, + plan=plan, + service_factory=EntitiesService, + ) + return EntitySetResolution( + entities=entities, + entities_service=resolution_service, + ) + + @traced(name="resolve_entity_set", run_type="uipath") + async def resolve_entity_set_async( + self, + items: list[DataFabricEntityItem], + ) -> EntitySetResolution: + """Resolve an agent entity set, applying resource overwrites.""" + + async def _resolve_folder_path(folder_path: str) -> Optional[str]: + if self._folders_service is None: + return None + return await self._folders_service.retrieve_folder_key_async(folder_path) + + plan = await create_resolution_plan_async( + items, + _resource_overwrites.get() or {}, + _resolve_folder_path, + ) + entities = await fetch_resolved_entities_async( + plan, + self.retrieve_async, + self.retrieve_by_name_async, + logger, + ) + resolution_service: EntitiesService = build_resolution_service( # type: ignore[assignment] + config=self._config, + execution_context=self._execution_context, + folders_service=self._folders_service, + plan=plan, + service_factory=EntitiesService, + ) + return EntitySetResolution( + entities=entities, + entities_service=resolution_service, ) def _query_entities_for_records( self, sql_query: str, - *, - routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: self._validate_sql_query(sql_query) + routing_context = self._build_routing_context_from_map() spec = self._query_entity_records_spec(sql_query, routing_context) response = self.request(spec.method, spec.endpoint, json=spec.json) return response.json().get("results", []) @@ -485,10 +637,9 @@ def _query_entities_for_records( async def _query_entities_for_records_async( self, sql_query: str, - *, - routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: self._validate_sql_query(sql_query) + routing_context = await self._build_routing_context_from_map_async() spec = self._query_entity_records_spec(sql_query, routing_context) response = await self.request_async(spec.method, spec.endpoint, json=spec.json) return response.json().get("results", []) @@ -956,6 +1107,21 @@ def _retrieve_spec( endpoint=Endpoint(f"datafabric_/api/Entity/{entity_key}"), ) + def _retrieve_by_name_spec( + self, + entity_name: str, + ) -> RequestSpec: + return RequestSpec( + method="GET", + endpoint=Endpoint(f"datafabric_/api/Entity/{entity_name}/metadata"), + ) + + @staticmethod + def _folder_key_headers(folder_key: Optional[str]) -> dict[str, str]: + if folder_key: + return {HEADER_FOLDER_KEY: folder_key} + return {} + def _list_entities_spec(self) -> RequestSpec: return RequestSpec( method="GET", @@ -992,6 +1158,152 @@ def _query_entity_records_spec( json=body, ) + def _build_routing_context_from_map( + self, + ) -> Optional[QueryRoutingOverrideContext]: + """Build a routing context from the configured folders_map and context overwrites. + + If a pre-built routing context was provided (e.g. by + ``resolve_entity_set_async``), it is returned directly without + re-resolving folder paths. + + Otherwise, folder paths in the map are resolved to folder keys via + FolderService and entity overwrites from the active + ``ResourceOverwritesContext`` are merged in. + + Returns: + A QueryRoutingOverrideContext if routing entries exist, + None otherwise. + """ + if self._routing_context is not None: + return self._routing_context + resolved = self._resolve_folder_paths_to_ids() + return self._build_routing_context_from_resolved_map(resolved) + + async def _build_routing_context_from_map_async( + self, + ) -> Optional[QueryRoutingOverrideContext]: + """Async version of _build_routing_context_from_map.""" + if self._routing_context is not None: + return self._routing_context + resolved = await self._resolve_folder_paths_to_ids_async() + return self._build_routing_context_from_resolved_map(resolved) + + def _resolve_folder_paths_to_ids(self) -> Optional[dict[str, str]]: + entity_overwrites = self._get_entity_overwrites_from_context() + folder_paths = set(self._folders_map.values()) + for overwrite in entity_overwrites.values(): + if overwrite.folder_path: + folder_paths.add(overwrite.folder_path) + + if not folder_paths: + return None + + resolved: dict[str, str] = {} + for folder_path in folder_paths: + if self._folders_service is not None: + folder_key = self._folders_service.retrieve_folder_key(folder_path) + if folder_key is not None: + resolved[folder_path] = folder_key + continue + resolved[folder_path] = folder_path + + return resolved + + async def _resolve_folder_paths_to_ids_async(self) -> Optional[dict[str, str]]: + entity_overwrites = self._get_entity_overwrites_from_context() + folder_paths = set(self._folders_map.values()) + for overwrite in entity_overwrites.values(): + if overwrite.folder_path: + folder_paths.add(overwrite.folder_path) + + if not folder_paths: + return None + + resolved: dict[str, str] = {} + for folder_path in folder_paths: + if self._folders_service is not None: + folder_key = await self._folders_service.retrieve_folder_key_async( + folder_path + ) + if folder_key is not None: + resolved[folder_path] = folder_key + continue + resolved[folder_path] = folder_path + + return resolved + + @staticmethod + def _get_entity_overwrites_from_context() -> Dict[str, EntityResourceOverwrite]: + """Extract entity overwrites from the active ResourceOverwritesContext. + + Returns: + A dict mapping original entity name to its EntityResourceOverwrite. + """ + context_overwrites = _resource_overwrites.get() + if not context_overwrites: + return {} + + result: Dict[str, EntityResourceOverwrite] = {} + for key, overwrite in context_overwrites.items(): + if isinstance(overwrite, EntityResourceOverwrite): + # Key format is "entity." + original_name = key.split(".", 1)[1] if "." in key else key + result[original_name] = overwrite + return result + + def _build_routing_context_from_resolved_map( + self, + resolved: Optional[dict[str, str]], + ) -> Optional[QueryRoutingOverrideContext]: + if self._folders_map: + return build_resolution_routing_context( + { + name: (resolved or {}).get(folder_path, folder_path) + for name, folder_path in self._folders_map.items() + }, + self._effective_entity_names, + ) + + routings: List[EntityRouting] = [] + if not self._folders_map: + # Fallback for direct SDK usage (no folders_map) + entity_overwrites = self._get_entity_overwrites_from_context() + for original_name, overwrite in entity_overwrites.items(): + override_name = ( + overwrite.resource_identifier + if overwrite.resource_identifier != original_name + else None + ) + routings.append( + EntityRouting( + entity_name=original_name, + folder_id=self._resolve_overwrite_folder(overwrite, resolved), + override_entity_name=override_name, + ) + ) + + if not routings: + return None + + return QueryRoutingOverrideContext(entity_routings=routings) + + @staticmethod + def _resolve_overwrite_folder( + overwrite: EntityResourceOverwrite, + resolved: Optional[dict[str, str]], + ) -> str: + """Return the folder key for an entity overwrite. + + Uses folder_id directly when present (already a key). + Falls back to resolving folder_path through the resolved map. + """ + if overwrite.folder_id: + return overwrite.folder_id + if overwrite.folder_path and resolved: + return resolved.get(overwrite.folder_path, overwrite.folder_path) + return overwrite.folder_identifier + def _insert_batch_spec(self, entity_key: str, records: List[Any]) -> RequestSpec: return RequestSpec( method="POST", @@ -1117,3 +1429,9 @@ def _projection_column_count( if not text: return 0 return len([part for part in text.split(",") if part.strip()]) + + +# Resolve the forward reference to EntitiesService in EntitySetResolution. +# The model uses TYPE_CHECKING to avoid circular imports in entities.py, +# so we must rebuild it here where EntitiesService is fully defined. +EntitySetResolution.model_rebuild() diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entity_resolution.py b/packages/uipath-platform/src/uipath/platform/entities/_entity_resolution.py new file mode 100644 index 000000000..875123f71 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/entities/_entity_resolution.py @@ -0,0 +1,266 @@ +from __future__ import annotations + +import asyncio +import logging +from dataclasses import dataclass +from typing import Awaitable, Callable, Optional + +from ..common._bindings import EntityResourceOverwrite, ResourceOverwrite +from ..common._config import UiPathApiConfig +from ..common._execution_context import UiPathExecutionContext +from ..orchestrator._folder_service import FolderService +from .entities import ( + DataFabricEntityItem, + Entity, + EntityRouting, + QueryRoutingOverrideContext, +) + +FolderPathResolver = Callable[[str], Optional[str]] +AsyncFolderPathResolver = Callable[[str], Awaitable[Optional[str]]] +EntityByKeyFetcher = Callable[[str], Entity] +AsyncEntityByKeyFetcher = Callable[[str], Awaitable[Entity]] +EntityByNameFetcher = Callable[[str, Optional[str]], Entity] +AsyncEntityByNameFetcher = Callable[[str, Optional[str]], Awaitable[Entity]] + + +@dataclass(frozen=True) +class EntityFetchByKey: + entity_key: str + + +@dataclass(frozen=True) +class EntityFetchByName: + entity_name: str + folder_key: str + + +@dataclass(frozen=True) +class EntityResolutionDraft: + fetch_by_key: list[EntityFetchByKey] + fetch_by_name: list[EntityFetchByName] + folders_map: dict[str, str] + effective_entity_names: dict[str, str] + folder_paths_to_resolve: set[str] + + +@dataclass(frozen=True) +class EntityResolutionPlan: + fetch_by_key: list[EntityFetchByKey] + fetch_by_name: list[EntityFetchByName] + folders_map: dict[str, str] + effective_entity_names: dict[str, str] + routing_context: QueryRoutingOverrideContext | None + + +def create_resolution_draft( + items: list[DataFabricEntityItem], + context_overwrites: dict[str, ResourceOverwrite], +) -> EntityResolutionDraft: + folders_map: dict[str, str] = {} + effective_entity_names: dict[str, str] = {} + folder_paths_to_resolve: set[str] = set() + fetch_by_key: list[EntityFetchByKey] = [] + fetch_by_name: list[EntityFetchByName] = [] + + for item in items: + overwrite = context_overwrites.get(f"entity.{item.id}") + resolved_folder = item.folder_key + + if isinstance(overwrite, EntityResourceOverwrite): + if overwrite.folder_id: + resolved_folder = overwrite.folder_id + elif overwrite.folder_path: + resolved_folder = overwrite.folder_path + folder_paths_to_resolve.add(overwrite.folder_path) + + if overwrite.name != item.name: + effective_entity_names[item.name] = overwrite.name + fetch_by_name.append( + EntityFetchByName( + entity_name=overwrite.name, + folder_key=resolved_folder, + ) + ) + folders_map[item.name] = resolved_folder + continue + + fetch_by_key.append(EntityFetchByKey(entity_key=item.entity_key or item.id)) + folders_map[item.name] = resolved_folder + + return EntityResolutionDraft( + fetch_by_key=fetch_by_key, + fetch_by_name=fetch_by_name, + folders_map=folders_map, + effective_entity_names=effective_entity_names, + folder_paths_to_resolve=folder_paths_to_resolve, + ) + + +def finalize_resolution_plan( + draft: EntityResolutionDraft, + resolve_folder_path: Callable[[str], Optional[str]], +) -> EntityResolutionPlan: + resolved_paths: dict[str, str] = {} + for folder_path in draft.folder_paths_to_resolve: + resolved_paths[folder_path] = resolve_folder_path(folder_path) or folder_path + + resolved_folders_map = { + entity_name: resolved_paths.get(folder_key, folder_key) + for entity_name, folder_key in draft.folders_map.items() + } + resolved_fetch_by_name = [ + EntityFetchByName( + entity_name=entry.entity_name, + folder_key=resolved_paths.get(entry.folder_key, entry.folder_key), + ) + for entry in draft.fetch_by_name + ] + + return EntityResolutionPlan( + fetch_by_key=draft.fetch_by_key, + fetch_by_name=resolved_fetch_by_name, + folders_map=resolved_folders_map, + effective_entity_names=draft.effective_entity_names, + routing_context=build_resolution_routing_context( + resolved_folders_map, + draft.effective_entity_names, + ), + ) + + +def build_resolution_routing_context( + folders_map: dict[str, str], + effective_entity_names: dict[str, str], +) -> QueryRoutingOverrideContext | None: + routings = [ + EntityRouting( + entity_name=effective_entity_names.get(original_name, original_name), + folder_id=folder_id, + override_entity_name=( + original_name if original_name in effective_entity_names else None + ), + ) + for original_name, folder_id in folders_map.items() + ] + if not routings: + return None + + return QueryRoutingOverrideContext(entity_routings=routings) + + +def create_resolution_plan( + items: list[DataFabricEntityItem], + context_overwrites: dict[str, ResourceOverwrite], + resolve_folder_path: FolderPathResolver, +) -> EntityResolutionPlan: + draft = create_resolution_draft(items, context_overwrites) + return finalize_resolution_plan(draft, resolve_folder_path) + + +async def create_resolution_plan_async( + items: list[DataFabricEntityItem], + context_overwrites: dict[str, ResourceOverwrite], + resolve_folder_path: AsyncFolderPathResolver, +) -> EntityResolutionPlan: + draft = create_resolution_draft(items, context_overwrites) + resolved_paths: dict[str, str] = {} + for folder_path in draft.folder_paths_to_resolve: + resolved_paths[folder_path] = ( + await resolve_folder_path(folder_path) + ) or folder_path + + return finalize_resolution_plan( + draft, + lambda folder_path: resolved_paths.get(folder_path, folder_path), + ) + + +def fetch_resolved_entities( + plan: EntityResolutionPlan, + retrieve_by_key: EntityByKeyFetcher, + retrieve_by_name: EntityByNameFetcher, + logger: logging.Logger, +) -> list[Entity]: + entities: list[Entity] = [] + for key_entry in plan.fetch_by_key: + try: + entities.append(retrieve_by_key(key_entry.entity_key)) + except Exception: + logger.warning( + "Failed to fetch entity by key '%s', skipping.", + key_entry.entity_key, + exc_info=True, + ) + + for name_entry in plan.fetch_by_name: + try: + entities.append( + retrieve_by_name(name_entry.entity_name, name_entry.folder_key) + ) + except Exception: + logger.warning( + "Failed to fetch entity by name '%s' (folder_key=%s), skipping.", + name_entry.entity_name, + name_entry.folder_key, + exc_info=True, + ) + + return entities + + +async def fetch_resolved_entities_async( + plan: EntityResolutionPlan, + retrieve_by_key: AsyncEntityByKeyFetcher, + retrieve_by_name: AsyncEntityByNameFetcher, + logger: logging.Logger, +) -> list[Entity]: + async def _safe_fetch_by_key(entry: EntityFetchByKey) -> Optional[Entity]: + try: + return await retrieve_by_key(entry.entity_key) + except Exception: + logger.warning( + "Failed to fetch entity by key '%s', skipping.", + entry.entity_key, + exc_info=True, + ) + return None + + async def _safe_fetch_by_name(entry: EntityFetchByName) -> Optional[Entity]: + try: + return await retrieve_by_name( + entry.entity_name, + entry.folder_key, + ) + except Exception: + logger.warning( + "Failed to fetch entity by name '%s' (folder_key=%s), skipping.", + entry.entity_name, + entry.folder_key, + exc_info=True, + ) + return None + + tasks = [_safe_fetch_by_key(entry) for entry in plan.fetch_by_key] + [ + _safe_fetch_by_name(entry) for entry in plan.fetch_by_name + ] + results = await asyncio.gather(*tasks) + return [entity for entity in results if entity is not None] + + +def build_resolution_service( + *, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + folders_service: FolderService | None, + plan: EntityResolutionPlan, + service_factory: Callable[..., object], +) -> object: + return service_factory( + config=config, + execution_context=execution_context, + folders_service=folders_service, + folders_map=plan.folders_map, + entity_name_overrides=plan.effective_entity_names, + routing_context=plan.routing_context, + ) diff --git a/packages/uipath-platform/src/uipath/platform/entities/entities.py b/packages/uipath-platform/src/uipath/platform/entities/entities.py index b2c49b763..b14f308d7 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/entities.py +++ b/packages/uipath-platform/src/uipath/platform/entities/entities.py @@ -1,11 +1,26 @@ """Entities models for UiPath Platform API interactions.""" +from __future__ import annotations + from enum import Enum from types import EllipsisType -from typing import Any, Dict, List, Optional, Type, Union, get_args, get_origin +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Optional, + Type, + Union, + get_args, + get_origin, +) from pydantic import BaseModel, ConfigDict, Field, create_model +if TYPE_CHECKING: + from ._entities_service import EntitiesService + class ReferenceType(Enum): """Enum representing types of references between entities.""" @@ -342,4 +357,27 @@ class QueryRoutingOverrideContext(BaseModel): entity_routings: List[EntityRouting] = Field(alias="entityRoutings") +class DataFabricEntityItem(BaseModel): + """A single Data Fabric entity reference from agent configuration.""" + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + id: str + entity_key: Optional[str] = Field(None, alias="referenceKey") + name: str + folder_key: str = Field(alias="folderId") + description: Optional[str] = None + + +class EntitySetResolution(BaseModel): + """Result of resolving an agent entity set with overwrites applied.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + entities: list[Entity] + entities_service: EntitiesService + + Entity.model_rebuild() diff --git a/packages/uipath-platform/tests/services/test_entities_service.py b/packages/uipath-platform/tests/services/test_entities_service.py index 8a9abafef..8d7c4ebf3 100644 --- a/packages/uipath-platform/tests/services/test_entities_service.py +++ b/packages/uipath-platform/tests/services/test_entities_service.py @@ -8,7 +8,11 @@ from pytest_httpx import HTTPXMock from uipath.platform import UiPathApiConfig, UiPathExecutionContext -from uipath.platform.entities import Entity, EntityRouting, QueryRoutingOverrideContext +from uipath.platform.common._bindings import ( + EntityResourceOverwrite, + _resource_overwrites, +) +from uipath.platform.entities import DataFabricEntityItem, Entity from uipath.platform.entities._entities_service import EntitiesService @@ -390,28 +394,21 @@ async def test_query_entity_records_async_calls_request_for_valid_sql( assert result == [{"id": "c1"}] service.request_async.assert_called_once() - def test_query_entity_records_with_routing_context( + def test_query_entity_records_builds_routing_context_from_folders_map( self, - service: EntitiesService, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, ) -> None: + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_map={"Customers": "solution_folder", "Orders": "folder-2"}, + ) response = MagicMock() response.json.return_value = {"results": [{"id": 1}]} service.request = MagicMock(return_value=response) # type: ignore[method-assign] - routing = QueryRoutingOverrideContext( - entity_routings=[ - EntityRouting(entity_name="Customers", folder_id="folder-1"), - EntityRouting( - entity_name="Orders", - folder_id="folder-2", - override_entity_name="OrdersV2", - ), - ] - ) - - result = service.query_entity_records( - "SELECT id FROM Customers LIMIT 10", routing_context=routing - ) + result = service.query_entity_records("SELECT id FROM Customers LIMIT 10") assert result == [{"id": 1}] call_kwargs = service.request.call_args @@ -419,39 +416,38 @@ def test_query_entity_records_with_routing_context( assert body["query"] == "SELECT id FROM Customers LIMIT 10" assert body["routingContext"] == { "entityRoutings": [ - {"entityName": "Customers", "folderId": "folder-1"}, - { - "entityName": "Orders", - "folderId": "folder-2", - "overrideEntityName": "OrdersV2", - }, + {"entityName": "Customers", "folderId": "solution_folder"}, + {"entityName": "Orders", "folderId": "folder-2"}, ] } @pytest.mark.anyio - async def test_query_entity_records_async_with_routing_context( + async def test_query_entity_records_async_builds_routing_context_from_folders_map( self, - service: EntitiesService, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, ) -> None: + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_map={"Customers": "solution_folder"}, + ) response = MagicMock() response.json.return_value = {"results": [{"id": "c1"}]} service.request_async = AsyncMock(return_value=response) # type: ignore[method-assign] - routing = QueryRoutingOverrideContext( - entity_routings=[ - EntityRouting(entity_name="Customers", folder_id="folder-1"), - ] - ) - result = await service.query_entity_records_async( - "SELECT id FROM Customers WHERE id = 'c1'", - routing_context=routing, + "SELECT id FROM Customers WHERE id = 'c1'" ) assert result == [{"id": "c1"}] call_kwargs = service.request_async.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") - assert "routingContext" in body + assert body["routingContext"] == { + "entityRoutings": [ + {"entityName": "Customers", "folderId": "solution_folder"}, + ] + } def test_query_entity_records_without_routing_context_omits_key( self, @@ -466,3 +462,316 @@ def test_query_entity_records_without_routing_context_omits_key( call_kwargs = service.request.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") assert "routingContext" not in body + + def test_query_entity_records_picks_up_entity_overwrites_from_context( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + from uipath.platform.common._bindings import ( + EntityResourceOverwrite, + _resource_overwrites, + ) + + service = EntitiesService( + config=config, + execution_context=execution_context, + ) + response = MagicMock() + response.json.return_value = {"results": [{"id": 1}]} + service.request = MagicMock(return_value=response) # type: ignore[method-assign] + + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Overwritten Customers", + folder_id="overwritten-folder-id", + ) + token = _resource_overwrites.set({"entity.Customers": overwrite}) + try: + service.query_entity_records("SELECT id FROM Customers LIMIT 10") + finally: + _resource_overwrites.reset(token) + + call_kwargs = service.request.call_args + body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + assert body["routingContext"] == { + "entityRoutings": [ + { + "entityName": "Customers", + "folderId": "overwritten-folder-id", + "overrideEntityName": "Overwritten Customers", + }, + ] + } + + def test_query_entity_records_merges_folders_map_with_entity_name_overrides( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_map={ + "Customers": "overwritten-folder-id", + "Orders": "orders-folder", + }, + entity_name_overrides={"Customers": "Overwritten Customers"}, + ) + response = MagicMock() + response.json.return_value = {"results": []} + service.request = MagicMock(return_value=response) # type: ignore[method-assign] + + service.query_entity_records("SELECT id FROM Customers LIMIT 10") + + call_kwargs = service.request.call_args + body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + routings = body["routingContext"]["entityRoutings"] + assert { + "entityName": "Overwritten Customers", + "folderId": "overwritten-folder-id", + "overrideEntityName": "Customers", + } in routings + assert {"entityName": "Orders", "folderId": "orders-folder"} in routings + # Exactly two routings — no duplicates + assert len(routings) == 2 + + def test_resolve_entity_set_uses_effective_sql_name_in_routing_context( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + service = EntitiesService( + config=config, + execution_context=execution_context, + ) + service.retrieve_by_name = MagicMock( # type: ignore[method-assign] + return_value=MagicMock(spec=Entity) + ) + + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Overwritten Customers", + folder_id="known-folder-key", + ) + token = _resource_overwrites.set({"entity.entity-1": overwrite}) + try: + resolution = service.resolve_entity_set( + [ + DataFabricEntityItem( + id="entity-1", + name="Customers", + folderId="original-folder-key", + ) + ] + ) + finally: + _resource_overwrites.reset(token) + + assert resolution.entities_service._routing_context is not None + assert resolution.entities_service._routing_context.model_dump( + by_alias=True, exclude_none=True + ) == { + "entityRoutings": [ + { + "entityName": "Overwritten Customers", + "folderId": "known-folder-key", + "overrideEntityName": "Customers", + } + ] + } + service.retrieve_by_name.assert_called_once_with( + "Overwritten Customers", + "known-folder-key", + ) + + @pytest.mark.asyncio + async def test_resolve_entity_set_async_resolves_folder_paths_before_fetch( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + folders_service = MagicMock() + folders_service.retrieve_folder_key_async = AsyncMock( + return_value="resolved-folder-id" + ) + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_service=folders_service, + ) + service.retrieve_by_name_async = AsyncMock( # type: ignore[method-assign] + return_value=MagicMock(spec=Entity) + ) + + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Overwritten Customers", + folder_path="Shared/Finance", + ) + token = _resource_overwrites.set({"entity.entity-1": overwrite}) + try: + resolution = await service.resolve_entity_set_async( + [ + DataFabricEntityItem( + id="entity-1", + name="Customers", + folderId="original-folder-key", + ) + ] + ) + finally: + _resource_overwrites.reset(token) + + folders_service.retrieve_folder_key_async.assert_awaited_once_with( + "Shared/Finance" + ) + assert resolution.entities_service._routing_context is not None + assert resolution.entities_service._routing_context.model_dump( + by_alias=True, exclude_none=True + ) == { + "entityRoutings": [ + { + "entityName": "Overwritten Customers", + "folderId": "resolved-folder-id", + "overrideEntityName": "Customers", + } + ] + } + service.retrieve_by_name_async.assert_awaited_once_with( + "Overwritten Customers", + "resolved-folder-id", + ) + + def test_query_entity_records_context_overwrite_same_name_no_override_field( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + from uipath.platform.common._bindings import ( + EntityResourceOverwrite, + _resource_overwrites, + ) + + service = EntitiesService( + config=config, + execution_context=execution_context, + ) + response = MagicMock() + response.json.return_value = {"results": []} + service.request = MagicMock(return_value=response) # type: ignore[method-assign] + + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Customers", + folder_id="different-folder-id", + ) + token = _resource_overwrites.set({"entity.Customers": overwrite}) + try: + service.query_entity_records("SELECT id FROM Customers LIMIT 10") + finally: + _resource_overwrites.reset(token) + + call_kwargs = service.request.call_args + body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + assert body["routingContext"] == { + "entityRoutings": [ + { + "entityName": "Customers", + "folderId": "different-folder-id", + }, + ] + } + + def test_query_entity_records_resolves_overwrite_folder_path_to_folder_key( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + from uipath.platform.common._bindings import ( + EntityResourceOverwrite, + _resource_overwrites, + ) + + folders_service = MagicMock() + folders_service.retrieve_folder_key.return_value = "resolved-folder-id" + + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_service=folders_service, + ) + response = MagicMock() + response.json.return_value = {"results": []} + service.request = MagicMock(return_value=response) # type: ignore[method-assign] + + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Overwritten Customers", + folder_path="Shared/Finance", + ) + token = _resource_overwrites.set({"entity.Customers": overwrite}) + try: + service.query_entity_records("SELECT id FROM Customers LIMIT 10") + finally: + _resource_overwrites.reset(token) + + call_kwargs = service.request.call_args + body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + assert body["routingContext"] == { + "entityRoutings": [ + { + "entityName": "Customers", + "folderId": "resolved-folder-id", + "overrideEntityName": "Overwritten Customers", + }, + ] + } + + def test_query_entity_records_uses_folder_id_directly_without_resolution( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + from uipath.platform.common._bindings import ( + EntityResourceOverwrite, + _resource_overwrites, + ) + + folders_service = MagicMock() + folders_service.retrieve_folder_key.return_value = None + + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_service=folders_service, + ) + response = MagicMock() + response.json.return_value = {"results": []} + service.request = MagicMock(return_value=response) # type: ignore[method-assign] + + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Overwritten Customers", + folder_id="known-folder-key", + ) + token = _resource_overwrites.set({"entity.Customers": overwrite}) + try: + service.query_entity_records("SELECT id FROM Customers LIMIT 10") + finally: + _resource_overwrites.reset(token) + + # folder_id is a key — should NOT be sent through FolderService + folders_service.retrieve_folder_key.assert_not_called() + + call_kwargs = service.request.call_args + body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + assert body["routingContext"] == { + "entityRoutings": [ + { + "entityName": "Customers", + "folderId": "known-folder-key", + "overrideEntityName": "Overwritten Customers", + }, + ] + } diff --git a/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md index 4af1b60ae..e524e4392 100644 --- a/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md +++ b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md @@ -500,10 +500,16 @@ sdk.entities.list_records(entity_key: str, schema: Optional[Type[Any]]=None, sta sdk.entities.list_records_async(entity_key: str, schema: Optional[Type[Any]]=None, start: Optional[int]=None, limit: Optional[int]=None) -> typing.List[uipath.platform.entities.entities.EntityRecord] # Query entity records using a validated SQL query. -sdk.entities.query_entity_records(sql_query: str, routing_context: Optional[uipath.platform.entities.entities.QueryRoutingOverrideContext]=None) -> typing.List[typing.Dict[str, typing.Any]] +sdk.entities.query_entity_records(sql_query: str) -> typing.List[typing.Dict[str, typing.Any]] # Asynchronously query entity records using a validated SQL query. -sdk.entities.query_entity_records_async(sql_query: str, routing_context: Optional[uipath.platform.entities.entities.QueryRoutingOverrideContext]=None) -> typing.List[typing.Dict[str, typing.Any]] +sdk.entities.query_entity_records_async(sql_query: str) -> typing.List[typing.Dict[str, typing.Any]] + +# Resolve an agent entity set, applying resource overwrites. +sdk.entities.resolve_entity_set(items: list[uipath.platform.entities.entities.DataFabricEntityItem]) -> uipath.platform.entities.entities.EntitySetResolution + +# Resolve an agent entity set, applying resource overwrites. +sdk.entities.resolve_entity_set_async(items: list[uipath.platform.entities.entities.DataFabricEntityItem]) -> uipath.platform.entities.entities.EntitySetResolution # Retrieve an entity by its key. sdk.entities.retrieve(entity_key: str) -> uipath.platform.entities.entities.Entity @@ -511,12 +517,21 @@ sdk.entities.retrieve(entity_key: str) -> uipath.platform.entities.entities.Enti # Asynchronously retrieve an entity by its key. sdk.entities.retrieve_async(entity_key: str) -> uipath.platform.entities.entities.Entity +# Retrieve an entity by its name. +sdk.entities.retrieve_by_name(entity_name: str, folder_key: Optional[str]=None) -> uipath.platform.entities.entities.Entity + +# Asynchronously retrieve an entity by its name. +sdk.entities.retrieve_by_name_async(entity_name: str, folder_key: Optional[str]=None) -> uipath.platform.entities.entities.Entity + # Update multiple records in an entity in a single batch operation. sdk.entities.update_records(entity_key: str, records: List[Any], schema: Optional[Type[Any]]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse # Asynchronously update multiple records in an entity in a single batch operation. sdk.entities.update_records_async(entity_key: str, records: List[Any], schema: Optional[Type[Any]]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse +# Return a new EntitiesService configured with the given folders map. +sdk.entities.with_folders_map(folders_map: Dict[str, str], entity_name_overrides: Optional[Dict[str, str]]=None) -> EntitiesService + ``` ### Folders diff --git a/packages/uipath/src/uipath/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 901ce7ca9..b9eaa656d 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -35,6 +35,7 @@ ) from uipath.eval.mocks import ExampleCall from uipath.platform.connections import Connection +from uipath.platform.entities import DataFabricEntityItem from uipath.platform.guardrails import ( BuiltInValidatorGuardrail, ) @@ -394,16 +395,6 @@ class AgentContextSettings(BaseCfg): ) -class DataFabricEntityItem(BaseCfg): - """A single Data Fabric entity reference.""" - - id: str - reference_key: Optional[str] = Field(None, alias="referenceKey") - name: str - folder_id: str = Field(alias="folderId") - description: Optional[str] = None - - class AgentContextResourceConfig(BaseAgentResourceConfig): """Agent context resource configuration model.""" diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index 1ddd7d102..d730f7e31 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -3529,10 +3529,10 @@ def test_datafabric_context_config_parses(self): assert len(parsed.entity_set) == 2 assert parsed.entity_set[0].id == "abc-123" assert parsed.entity_set[0].name == "Customers" - assert parsed.entity_set[0].folder_id == "folder-1" + assert parsed.entity_set[0].folder_key == "folder-1" assert parsed.entity_set[0].description == "Customer records" - assert parsed.entity_set[0].reference_key is None - assert parsed.entity_set[1].reference_key == "orders-ref" + assert parsed.entity_set[0].entity_key is None + assert parsed.entity_set[1].entity_key == "orders-ref" assert parsed.entity_set[1].description is None def test_is_datafabric(self): diff --git a/packages/uipath/tests/resource_overrides/overwrites.json b/packages/uipath/tests/resource_overrides/overwrites.json index c58744a69..e0bca84ba 100644 --- a/packages/uipath/tests/resource_overrides/overwrites.json +++ b/packages/uipath/tests/resource_overrides/overwrites.json @@ -28,5 +28,9 @@ "mcpServer.mcp_server_name": { "name": "Overwritten MCP Server Name", "folderPath": "Overwritten/MCPServer/Folder" + }, + "entity.entity_name": { + "name": "Overwritten Entity Name", + "folderId": "overwritten-entity-folder-id-123" } } \ No newline at end of file diff --git a/packages/uipath/tests/resource_overrides/test_resource_overrides.py b/packages/uipath/tests/resource_overrides/test_resource_overrides.py index c15bc113b..8d39a762d 100644 --- a/packages/uipath/tests/resource_overrides/test_resource_overrides.py +++ b/packages/uipath/tests/resource_overrides/test_resource_overrides.py @@ -310,6 +310,11 @@ def test_parse_overwrites_with_type_adapter(self, overwrites_data): assert mcp_server.resource_identifier == "Overwritten MCP Server Name" assert mcp_server.folder_identifier == "Overwritten/MCPServer/Folder" + # Verify entity overwrite + entity = parsed_overwrites["entity.entity_name"] + assert entity.resource_identifier == "Overwritten Entity Name" + assert entity.folder_identifier == "overwritten-entity-folder-id-123" + def test_overrides_decorator_should_pop_kwargs_dict_when_present(self): from uipath.platform.common import resource_override From c181f231727d488a06b7fed76fe3bc3e7afaaf4c Mon Sep 17 00:00:00 2001 From: Harshit Rohatgi Date: Wed, 8 Apr 2026 13:29:20 +0530 Subject: [PATCH 3/3] fix(platform): support entity resource overwrites with folder resolution --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/common/_bindings.py | 4 +- .../platform/entities/_entities_service.py | 191 +----------- .../platform/entities/_entity_resolution.py | 272 +++++++++++++++++- .../tests/services/test_entities_service.py | 24 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- .../src/uipath/_resources/SDK_REFERENCE.md | 2 - packages/uipath/uv.lock | 4 +- 9 files changed, 299 insertions(+), 204 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index b53fb68f9..44f48463f 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.21" +version = "0.1.22" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_bindings.py b/packages/uipath-platform/src/uipath/platform/common/_bindings.py index 321d05694..321b83c4f 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_bindings.py +++ b/packages/uipath-platform/src/uipath/platform/common/_bindings.py @@ -155,9 +155,9 @@ def _normalize_value(resource_type: str, value: dict[str, Any]) -> dict[str, Any normalized = dict(value) if "folderId" in normalized: - normalized["folder_id"] = normalized["folderId"] + normalized["folder_id"] = normalized.pop("folderId") if "folderPath" in normalized: - normalized["folder_path"] = normalized["folderPath"] + normalized["folder_path"] = normalized.pop("folderPath") return normalized diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py index 2cd16f62c..43b437d65 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py +++ b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py @@ -8,17 +8,18 @@ from uipath.core.tracing import traced from ..common._base_service import BaseService -from ..common._bindings import EntityResourceOverwrite, _resource_overwrites +from ..common._bindings import _resource_overwrites from ..common._config import UiPathApiConfig from ..common._execution_context import UiPathExecutionContext from ..common._models import Endpoint, RequestSpec from ..common.constants import HEADER_FOLDER_KEY from ..orchestrator._folder_service import FolderService from ._entity_resolution import ( - build_resolution_routing_context, + RoutingStrategy, build_resolution_service, create_resolution_plan, create_resolution_plan_async, + create_routing_strategy, fetch_resolved_entities, fetch_resolved_entities_async, ) @@ -27,7 +28,6 @@ Entity, EntityRecord, EntityRecordsBatchResponse, - EntityRouting, EntitySetResolution, QueryRoutingOverrideContext, ) @@ -74,32 +74,11 @@ def __init__( ) -> None: super().__init__(config=config, execution_context=execution_context) self._folders_service = folders_service - self._folders_map = folders_map or {} - self._effective_entity_names = entity_name_overrides or {} - self._routing_context = routing_context - - def with_folders_map( - self, - folders_map: Dict[str, str], - entity_name_overrides: Optional[Dict[str, str]] = None, - ) -> "EntitiesService": - """Return a new EntitiesService configured with the given folders map. - - The map is used to build a routing context automatically when - ``query_entity_records`` is called without an explicit routing context. - Folder paths in the map are resolved to folder keys via ``FolderService``. - - Args: - folders_map: Mapping of entity name to folder path. - entity_name_overrides: Mapping of original entity name to - overridden entity name. - """ - return EntitiesService( - config=self._config, - execution_context=self._execution_context, - folders_service=self._folders_service, + self._routing_strategy: RoutingStrategy = create_routing_strategy( folders_map=folders_map, - entity_name_overrides=entity_name_overrides, + effective_entity_names=entity_name_overrides, + routing_context=routing_context, + folders_service=folders_service, ) @traced(name="entity_retrieve", run_type="uipath") @@ -566,7 +545,7 @@ def resolve_entity_set( items, _resource_overwrites.get() or {}, lambda folder_path: ( - self._folders_service.retrieve_folder_key(folder_path) + self._folders_service.retrieve_key(folder_path=folder_path) if self._folders_service is not None else None ), @@ -599,7 +578,9 @@ async def resolve_entity_set_async( async def _resolve_folder_path(folder_path: str) -> Optional[str]: if self._folders_service is None: return None - return await self._folders_service.retrieve_folder_key_async(folder_path) + return await self._folders_service.retrieve_key_async( + folder_path=folder_path + ) plan = await create_resolution_plan_async( items, @@ -629,7 +610,7 @@ def _query_entities_for_records( sql_query: str, ) -> List[Dict[str, Any]]: self._validate_sql_query(sql_query) - routing_context = self._build_routing_context_from_map() + routing_context = self._routing_strategy.resolve() spec = self._query_entity_records_spec(sql_query, routing_context) response = self.request(spec.method, spec.endpoint, json=spec.json) return response.json().get("results", []) @@ -639,7 +620,7 @@ async def _query_entities_for_records_async( sql_query: str, ) -> List[Dict[str, Any]]: self._validate_sql_query(sql_query) - routing_context = await self._build_routing_context_from_map_async() + routing_context = await self._routing_strategy.resolve_async() spec = self._query_entity_records_spec(sql_query, routing_context) response = await self.request_async(spec.method, spec.endpoint, json=spec.json) return response.json().get("results", []) @@ -1158,152 +1139,6 @@ def _query_entity_records_spec( json=body, ) - def _build_routing_context_from_map( - self, - ) -> Optional[QueryRoutingOverrideContext]: - """Build a routing context from the configured folders_map and context overwrites. - - If a pre-built routing context was provided (e.g. by - ``resolve_entity_set_async``), it is returned directly without - re-resolving folder paths. - - Otherwise, folder paths in the map are resolved to folder keys via - FolderService and entity overwrites from the active - ``ResourceOverwritesContext`` are merged in. - - Returns: - A QueryRoutingOverrideContext if routing entries exist, - None otherwise. - """ - if self._routing_context is not None: - return self._routing_context - resolved = self._resolve_folder_paths_to_ids() - return self._build_routing_context_from_resolved_map(resolved) - - async def _build_routing_context_from_map_async( - self, - ) -> Optional[QueryRoutingOverrideContext]: - """Async version of _build_routing_context_from_map.""" - if self._routing_context is not None: - return self._routing_context - resolved = await self._resolve_folder_paths_to_ids_async() - return self._build_routing_context_from_resolved_map(resolved) - - def _resolve_folder_paths_to_ids(self) -> Optional[dict[str, str]]: - entity_overwrites = self._get_entity_overwrites_from_context() - folder_paths = set(self._folders_map.values()) - for overwrite in entity_overwrites.values(): - if overwrite.folder_path: - folder_paths.add(overwrite.folder_path) - - if not folder_paths: - return None - - resolved: dict[str, str] = {} - for folder_path in folder_paths: - if self._folders_service is not None: - folder_key = self._folders_service.retrieve_folder_key(folder_path) - if folder_key is not None: - resolved[folder_path] = folder_key - continue - resolved[folder_path] = folder_path - - return resolved - - async def _resolve_folder_paths_to_ids_async(self) -> Optional[dict[str, str]]: - entity_overwrites = self._get_entity_overwrites_from_context() - folder_paths = set(self._folders_map.values()) - for overwrite in entity_overwrites.values(): - if overwrite.folder_path: - folder_paths.add(overwrite.folder_path) - - if not folder_paths: - return None - - resolved: dict[str, str] = {} - for folder_path in folder_paths: - if self._folders_service is not None: - folder_key = await self._folders_service.retrieve_folder_key_async( - folder_path - ) - if folder_key is not None: - resolved[folder_path] = folder_key - continue - resolved[folder_path] = folder_path - - return resolved - - @staticmethod - def _get_entity_overwrites_from_context() -> Dict[str, EntityResourceOverwrite]: - """Extract entity overwrites from the active ResourceOverwritesContext. - - Returns: - A dict mapping original entity name to its EntityResourceOverwrite. - """ - context_overwrites = _resource_overwrites.get() - if not context_overwrites: - return {} - - result: Dict[str, EntityResourceOverwrite] = {} - for key, overwrite in context_overwrites.items(): - if isinstance(overwrite, EntityResourceOverwrite): - # Key format is "entity." - original_name = key.split(".", 1)[1] if "." in key else key - result[original_name] = overwrite - return result - - def _build_routing_context_from_resolved_map( - self, - resolved: Optional[dict[str, str]], - ) -> Optional[QueryRoutingOverrideContext]: - if self._folders_map: - return build_resolution_routing_context( - { - name: (resolved or {}).get(folder_path, folder_path) - for name, folder_path in self._folders_map.items() - }, - self._effective_entity_names, - ) - - routings: List[EntityRouting] = [] - if not self._folders_map: - # Fallback for direct SDK usage (no folders_map) - entity_overwrites = self._get_entity_overwrites_from_context() - for original_name, overwrite in entity_overwrites.items(): - override_name = ( - overwrite.resource_identifier - if overwrite.resource_identifier != original_name - else None - ) - routings.append( - EntityRouting( - entity_name=original_name, - folder_id=self._resolve_overwrite_folder(overwrite, resolved), - override_entity_name=override_name, - ) - ) - - if not routings: - return None - - return QueryRoutingOverrideContext(entity_routings=routings) - - @staticmethod - def _resolve_overwrite_folder( - overwrite: EntityResourceOverwrite, - resolved: Optional[dict[str, str]], - ) -> str: - """Return the folder key for an entity overwrite. - - Uses folder_id directly when present (already a key). - Falls back to resolving folder_path through the resolved map. - """ - if overwrite.folder_id: - return overwrite.folder_id - if overwrite.folder_path and resolved: - return resolved.get(overwrite.folder_path, overwrite.folder_path) - return overwrite.folder_identifier - def _insert_batch_spec(self, entity_key: str, records: List[Any]) -> RequestSpec: return RequestSpec( method="POST", diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entity_resolution.py b/packages/uipath-platform/src/uipath/platform/entities/_entity_resolution.py index 875123f71..15876717c 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/_entity_resolution.py +++ b/packages/uipath-platform/src/uipath/platform/entities/_entity_resolution.py @@ -1,11 +1,16 @@ from __future__ import annotations +import abc import asyncio import logging from dataclasses import dataclass -from typing import Awaitable, Callable, Optional +from typing import Awaitable, Callable, Dict, Optional -from ..common._bindings import EntityResourceOverwrite, ResourceOverwrite +from ..common._bindings import ( + EntityResourceOverwrite, + ResourceOverwrite, + _resource_overwrites, +) from ..common._config import UiPathApiConfig from ..common._execution_context import UiPathExecutionContext from ..orchestrator._folder_service import FolderService @@ -24,6 +29,257 @@ AsyncEntityByNameFetcher = Callable[[str, Optional[str]], Awaitable[Entity]] +# --------------------------------------------------------------------------- +# Routing strategy +# --------------------------------------------------------------------------- + + +class RoutingStrategy(abc.ABC): + """Strategy for resolving a ``QueryRoutingOverrideContext`` at query time.""" + + @abc.abstractmethod + def resolve(self) -> Optional[QueryRoutingOverrideContext]: ... + + @abc.abstractmethod + async def resolve_async(self) -> Optional[QueryRoutingOverrideContext]: ... + + +class PreResolvedRoutingStrategy(RoutingStrategy): + """Returns a routing context that was fully resolved at init time. + + Used after ``resolve_entity_set`` where all folder paths have already + been converted to folder keys and the routing context is immutable. + """ + + def __init__( + self, + routing_context: QueryRoutingOverrideContext, + ) -> None: + self._routing_context = routing_context + + def resolve(self) -> Optional[QueryRoutingOverrideContext]: + return self._routing_context + + async def resolve_async(self) -> Optional[QueryRoutingOverrideContext]: + return self._routing_context + + @property + def routing_context(self) -> QueryRoutingOverrideContext: + return self._routing_context + + +class FoldersMapRoutingStrategy(RoutingStrategy): + """Builds a routing context from a pre-populated folders map. + + Used when an ``EntitiesService`` is constructed with an explicit + ``folders_map`` (and optional entity-name overrides) but *without* a + pre-built routing context. Folder paths in the map are resolved to + folder keys lazily at query time via ``FolderService``. + """ + + def __init__( + self, + folders_map: Dict[str, str], + effective_entity_names: Dict[str, str], + folders_service: Optional[FolderService], + ) -> None: + self._folders_map = folders_map + self._effective_entity_names = effective_entity_names + self._folders_service = folders_service + + def resolve(self) -> Optional[QueryRoutingOverrideContext]: + resolved = self._resolve_folder_paths() + return build_resolution_routing_context( + { + name: (resolved or {}).get(path, path) + for name, path in self._folders_map.items() + }, + self._effective_entity_names, + ) + + async def resolve_async(self) -> Optional[QueryRoutingOverrideContext]: + resolved = await self._resolve_folder_paths_async() + return build_resolution_routing_context( + { + name: (resolved or {}).get(path, path) + for name, path in self._folders_map.items() + }, + self._effective_entity_names, + ) + + def _resolve_folder_paths(self) -> Optional[dict[str, str]]: + folder_paths = set(self._folders_map.values()) + if not folder_paths: + return None + + resolved: dict[str, str] = {} + for folder_path in folder_paths: + if self._folders_service is not None: + folder_key = self._folders_service.retrieve_key(folder_path=folder_path) + if folder_key is not None: + resolved[folder_path] = folder_key + continue + resolved[folder_path] = folder_path + return resolved + + async def _resolve_folder_paths_async(self) -> Optional[dict[str, str]]: + folder_paths = set(self._folders_map.values()) + if not folder_paths: + return None + + resolved: dict[str, str] = {} + for folder_path in folder_paths: + if self._folders_service is not None: + folder_key = await self._folders_service.retrieve_key_async( + folder_path=folder_path + ) + if folder_key is not None: + resolved[folder_path] = folder_key + continue + resolved[folder_path] = folder_path + return resolved + + +class ContextOverwriteRoutingStrategy(RoutingStrategy): + """Builds a routing context lazily from ``_resource_overwrites``. + + This is the fallback for direct SDK usage where no ``folders_map`` or + pre-resolved routing context exists. Entity overwrites are read from + the active ``ResourceOverwritesContext`` at query time. + """ + + def __init__(self, folders_service: Optional[FolderService]) -> None: + self._folders_service = folders_service + + def resolve(self) -> Optional[QueryRoutingOverrideContext]: + entity_overwrites = _get_entity_overwrites_from_context() + if not entity_overwrites: + return None + + folder_paths = { + ow.folder_path for ow in entity_overwrites.values() if ow.folder_path + } + resolved = self._resolve_paths(folder_paths) + return self._build(entity_overwrites, resolved) + + async def resolve_async(self) -> Optional[QueryRoutingOverrideContext]: + entity_overwrites = _get_entity_overwrites_from_context() + if not entity_overwrites: + return None + + folder_paths = { + ow.folder_path for ow in entity_overwrites.values() if ow.folder_path + } + resolved = await self._resolve_paths_async(folder_paths) + return self._build(entity_overwrites, resolved) + + def _resolve_paths(self, folder_paths: set[str]) -> dict[str, str]: + resolved: dict[str, str] = {} + for path in folder_paths: + if self._folders_service is not None: + key = self._folders_service.retrieve_key(folder_path=path) + if key is not None: + resolved[path] = key + continue + resolved[path] = path + return resolved + + async def _resolve_paths_async(self, folder_paths: set[str]) -> dict[str, str]: + resolved: dict[str, str] = {} + for path in folder_paths: + if self._folders_service is not None: + key = await self._folders_service.retrieve_key_async(folder_path=path) + if key is not None: + resolved[path] = key + continue + resolved[path] = path + return resolved + + @staticmethod + def _build( + entity_overwrites: Dict[str, EntityResourceOverwrite], + resolved: dict[str, str], + ) -> Optional[QueryRoutingOverrideContext]: + routings: list[EntityRouting] = [] + for original_name, overwrite in entity_overwrites.items(): + override_name = ( + overwrite.resource_identifier + if overwrite.resource_identifier != original_name + else None + ) + folder_id = _resolve_overwrite_folder(overwrite, resolved) + routings.append( + EntityRouting( + entity_name=original_name, + folder_id=folder_id, + override_entity_name=override_name, + ) + ) + + if not routings: + return None + return QueryRoutingOverrideContext(entity_routings=routings) + + +def create_routing_strategy( + *, + folders_map: Optional[Dict[str, str]], + effective_entity_names: Optional[Dict[str, str]], + routing_context: Optional[QueryRoutingOverrideContext], + folders_service: Optional[FolderService], +) -> RoutingStrategy: + """Select the appropriate routing strategy based on init-time state.""" + if routing_context is not None: + return PreResolvedRoutingStrategy(routing_context) + if folders_map: + return FoldersMapRoutingStrategy( + folders_map, + effective_entity_names or {}, + folders_service, + ) + return ContextOverwriteRoutingStrategy(folders_service) + + +# --------------------------------------------------------------------------- +# Helpers shared across strategies +# --------------------------------------------------------------------------- + + +def _get_entity_overwrites_from_context() -> Dict[str, EntityResourceOverwrite]: + """Extract entity overwrites from the active ResourceOverwritesContext.""" + context_overwrites = _resource_overwrites.get() + if not context_overwrites: + return {} + + result: Dict[str, EntityResourceOverwrite] = {} + for key, overwrite in context_overwrites.items(): + if isinstance(overwrite, EntityResourceOverwrite): + original_name = key.split(".", 1)[1] if "." in key else key + result[original_name] = overwrite + return result + + +def _resolve_overwrite_folder( + overwrite: EntityResourceOverwrite, + resolved: dict[str, str], +) -> str: + """Return the folder key for an entity overwrite. + + Uses folder_id directly when present (already a key). + Falls back to resolving folder_path through the resolved map. + """ + if overwrite.folder_id: + return overwrite.folder_id + if overwrite.folder_path and resolved: + return resolved.get(overwrite.folder_path, overwrite.folder_path) + return overwrite.folder_identifier + + +# --------------------------------------------------------------------------- +# Resolution plan (used by resolve_entity_set) +# --------------------------------------------------------------------------- + + @dataclass(frozen=True) class EntityFetchByKey: entity_key: str @@ -64,18 +320,24 @@ def create_resolution_draft( fetch_by_name: list[EntityFetchByName] = [] for item in items: - overwrite = context_overwrites.get(f"entity.{item.id}") + overwrite = context_overwrites.get( + f"entity.{item.id}" + ) or context_overwrites.get(f"entity.{item.name}") resolved_folder = item.folder_key if isinstance(overwrite, EntityResourceOverwrite): + folder_changed = False if overwrite.folder_id: resolved_folder = overwrite.folder_id + folder_changed = resolved_folder != item.folder_key elif overwrite.folder_path: resolved_folder = overwrite.folder_path + folder_changed = True folder_paths_to_resolve.add(overwrite.folder_path) - if overwrite.name != item.name: - effective_entity_names[item.name] = overwrite.name + if overwrite.name != item.name or folder_changed: + if overwrite.name != item.name: + effective_entity_names[item.name] = overwrite.name fetch_by_name.append( EntityFetchByName( entity_name=overwrite.name, diff --git a/packages/uipath-platform/tests/services/test_entities_service.py b/packages/uipath-platform/tests/services/test_entities_service.py index 8d7c4ebf3..09294df7a 100644 --- a/packages/uipath-platform/tests/services/test_entities_service.py +++ b/packages/uipath-platform/tests/services/test_entities_service.py @@ -561,15 +561,15 @@ def test_resolve_entity_set_uses_effective_sql_name_in_routing_context( DataFabricEntityItem( id="entity-1", name="Customers", - folderId="original-folder-key", + folder_key="original-folder-key", ) ] ) finally: _resource_overwrites.reset(token) - assert resolution.entities_service._routing_context is not None - assert resolution.entities_service._routing_context.model_dump( + assert resolution.entities_service._routing_strategy.routing_context is not None + assert resolution.entities_service._routing_strategy.routing_context.model_dump( by_alias=True, exclude_none=True ) == { "entityRoutings": [ @@ -592,7 +592,7 @@ async def test_resolve_entity_set_async_resolves_folder_paths_before_fetch( execution_context: UiPathExecutionContext, ) -> None: folders_service = MagicMock() - folders_service.retrieve_folder_key_async = AsyncMock( + folders_service.retrieve_key_async = AsyncMock( return_value="resolved-folder-id" ) service = EntitiesService( @@ -616,18 +616,18 @@ async def test_resolve_entity_set_async_resolves_folder_paths_before_fetch( DataFabricEntityItem( id="entity-1", name="Customers", - folderId="original-folder-key", + folder_key="original-folder-key", ) ] ) finally: _resource_overwrites.reset(token) - folders_service.retrieve_folder_key_async.assert_awaited_once_with( - "Shared/Finance" + folders_service.retrieve_key_async.assert_awaited_once_with( + folder_path="Shared/Finance" ) - assert resolution.entities_service._routing_context is not None - assert resolution.entities_service._routing_context.model_dump( + assert resolution.entities_service._routing_strategy.routing_context is not None + assert resolution.entities_service._routing_strategy.routing_context.model_dump( by_alias=True, exclude_none=True ) == { "entityRoutings": [ @@ -694,7 +694,7 @@ def test_query_entity_records_resolves_overwrite_folder_path_to_folder_key( ) folders_service = MagicMock() - folders_service.retrieve_folder_key.return_value = "resolved-folder-id" + folders_service.retrieve_key.return_value = "resolved-folder-id" service = EntitiesService( config=config, @@ -739,7 +739,7 @@ def test_query_entity_records_uses_folder_id_directly_without_resolution( ) folders_service = MagicMock() - folders_service.retrieve_folder_key.return_value = None + folders_service.retrieve_key.return_value = None service = EntitiesService( config=config, @@ -762,7 +762,7 @@ def test_query_entity_records_uses_folder_id_directly_without_resolution( _resource_overwrites.reset(token) # folder_id is a key — should NOT be sent through FolderService - folders_service.retrieve_folder_key.assert_not_called() + folders_service.retrieve_key.assert_not_called() call_kwargs = service.request.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 648df8ab4..0494f3530 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.21" +version = "0.1.22" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 207f46657..4b1b0dc3f 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.43" +version = "2.10.44" 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/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md index e524e4392..2e6a4a585 100644 --- a/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md +++ b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md @@ -529,8 +529,6 @@ sdk.entities.update_records(entity_key: str, records: List[Any], schema: Optiona # Asynchronously update multiple records in an entity in a single batch operation. sdk.entities.update_records_async(entity_key: str, records: List[Any], schema: Optional[Type[Any]]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse -# Return a new EntitiesService configured with the given folders map. -sdk.entities.with_folders_map(folders_map: Dict[str, str], entity_name_overrides: Optional[Dict[str, str]]=None) -> EntitiesService ``` diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index b8a817758..9d9b569ab 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.43" +version = "2.10.44" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.21" +version = "0.1.22" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" },