Skip to content

Commit 11e86d4

Browse files
Fix/entity as resource overwrite (#1544)
1 parent f5058fb commit 11e86d4

12 files changed

Lines changed: 357 additions & 63 deletions

File tree

packages/uipath-platform/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-platform"
3-
version = "0.1.18"
3+
version = "0.1.19"
44
description = "HTTP client library for programmatic access to UiPath Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath-platform/src/uipath/platform/_uipath.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ def llm(self) -> UiPathLlmChatService:
139139

140140
@property
141141
def entities(self) -> EntitiesService:
142-
return EntitiesService(self._config, self._execution_context)
142+
return EntitiesService(
143+
self._config, self._execution_context, folders_service=self.folders
144+
)
143145

144146
@cached_property
145147
def resource_catalog(self) -> ResourceCatalogService:

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ._base_service import BaseService
88
from ._bindings import (
99
ConnectionResourceOverwrite,
10+
EntityResourceOverwrite,
1011
GenericResourceOverwrite,
1112
ResourceOverwrite,
1213
ResourceOverwriteParser,
@@ -100,6 +101,7 @@
100101
"EndpointManager",
101102
"jsonschema_to_pydantic",
102103
"ConnectionResourceOverwrite",
104+
"EntityResourceOverwrite",
103105
"GenericResourceOverwrite",
104106
"ResourceOverwrite",
105107
"ResourceOverwriteParser",

packages/uipath-platform/src/uipath/platform/common/_bindings.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def folder_identifier(self) -> str:
4545

4646
class GenericResourceOverwrite(ResourceOverwrite):
4747
resource_type: Literal[
48-
"process", "index", "app", "asset", "bucket", "mcpServer", "queue", "entity"
48+
"process", "index", "app", "asset", "bucket", "mcpServer", "queue"
4949
]
5050
name: str = Field(alias="name")
5151
folder_path: str = Field(alias="folderPath")
@@ -59,6 +59,20 @@ def folder_identifier(self) -> str:
5959
return self.folder_path
6060

6161

62+
class EntityResourceOverwrite(ResourceOverwrite):
63+
resource_type: Literal["entity"]
64+
name: str = Field(alias="name")
65+
folder_key: str = Field(alias="folderId")
66+
67+
@property
68+
def resource_identifier(self) -> str:
69+
return self.name
70+
71+
@property
72+
def folder_identifier(self) -> str:
73+
return self.folder_key
74+
75+
6276
class ConnectionResourceOverwrite(ResourceOverwrite):
6377
resource_type: Literal["connection"]
6478
# In eval context, studio web provides "ConnectionId".
@@ -83,7 +97,9 @@ def folder_identifier(self) -> str:
8397

8498

8599
ResourceOverwriteUnion = Annotated[
86-
Union[GenericResourceOverwrite, ConnectionResourceOverwrite],
100+
Union[
101+
GenericResourceOverwrite, EntityResourceOverwrite, ConnectionResourceOverwrite
102+
],
87103
Field(discriminator="resource_type"),
88104
]
89105

packages/uipath-platform/src/uipath/platform/entities/_entities_service.py

Lines changed: 167 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from typing import Any, Dict, List, Optional, Type
23

34
import sqlparse
@@ -7,16 +8,21 @@
78
from uipath.core.tracing import traced
89

910
from ..common._base_service import BaseService
11+
from ..common._bindings import EntityResourceOverwrite, _resource_overwrites
1012
from ..common._config import UiPathApiConfig
1113
from ..common._execution_context import UiPathExecutionContext
1214
from ..common._models import Endpoint, RequestSpec
15+
from ..orchestrator._folder_service import FolderService
1316
from .entities import (
1417
Entity,
1518
EntityRecord,
1619
EntityRecordsBatchResponse,
20+
EntityRouting,
1721
QueryRoutingOverrideContext,
1822
)
1923

24+
logger = logging.getLogger(__name__)
25+
2026
_FORBIDDEN_DML = {"INSERT", "UPDATE", "DELETE", "MERGE", "REPLACE"}
2127
_FORBIDDEN_DDL = {"DROP", "ALTER", "CREATE", "TRUNCATE"}
2228
_DISALLOWED_KEYWORDS = [
@@ -47,9 +53,32 @@ class EntitiesService(BaseService):
4753
"""
4854

4955
def __init__(
50-
self, config: UiPathApiConfig, execution_context: UiPathExecutionContext
56+
self,
57+
config: UiPathApiConfig,
58+
execution_context: UiPathExecutionContext,
59+
folders_service: Optional[FolderService] = None,
60+
folders_map: Optional[Dict[str, str]] = None,
5161
) -> None:
5262
super().__init__(config=config, execution_context=execution_context)
63+
self._folders_service = folders_service
64+
self._folders_map = folders_map or {}
65+
66+
def with_folders_map(self, folders_map: Dict[str, str]) -> "EntitiesService":
67+
"""Return a new EntitiesService configured with the given folders map.
68+
69+
The map is used to build a routing context automatically when
70+
``query_entity_records`` is called without an explicit routing context.
71+
Folder paths in the map are resolved to folder keys via ``FolderService``.
72+
73+
Args:
74+
folders_map: Mapping of entity name to folder path.
75+
"""
76+
return EntitiesService(
77+
config=self._config,
78+
execution_context=self._execution_context,
79+
folders_service=self._folders_service,
80+
folders_map=folders_map,
81+
)
5382

5483
@traced(name="entity_retrieve", run_type="uipath")
5584
def retrieve(self, entity_key: str) -> Entity:
@@ -417,7 +446,6 @@ class CustomerRecord:
417446
def query_entity_records(
418447
self,
419448
sql_query: str,
420-
routing_context: Optional[QueryRoutingOverrideContext] = None,
421449
) -> List[Dict[str, Any]]:
422450
"""Query entity records using a validated SQL query.
423451
@@ -427,9 +455,10 @@ def query_entity_records(
427455
sql_query (str): A SQL SELECT query to execute against Data Service entities.
428456
Only SELECT statements are allowed. Queries without WHERE must include
429457
a LIMIT clause. Subqueries and multi-statement queries are not permitted.
430-
routing_context (Optional[QueryRoutingOverrideContext]): Per-entity routing context
431-
for multi-folder queries. When present, included in the request body
432-
and takes precedence over the folder header on the backend.
458+
459+
Notes:
460+
A routing context is always derived from the configured ``folders_map``
461+
when present and included in the request body.
433462
434463
Returns:
435464
List[Dict[str, Any]]: A list of result records as dictionaries.
@@ -438,15 +467,12 @@ def query_entity_records(
438467
ValueError: If the SQL query fails validation (e.g., non-SELECT, missing
439468
WHERE/LIMIT, forbidden keywords, subqueries).
440469
"""
441-
return self._query_entities_for_records(
442-
sql_query, routing_context=routing_context
443-
)
470+
return self._query_entities_for_records(sql_query)
444471

445472
@traced(name="entity_query_records", run_type="uipath")
446473
async def query_entity_records_async(
447474
self,
448475
sql_query: str,
449-
routing_context: Optional[QueryRoutingOverrideContext] = None,
450476
) -> List[Dict[str, Any]]:
451477
"""Asynchronously query entity records using a validated SQL query.
452478
@@ -456,9 +482,10 @@ async def query_entity_records_async(
456482
sql_query (str): A SQL SELECT query to execute against Data Service entities.
457483
Only SELECT statements are allowed. Queries without WHERE must include
458484
a LIMIT clause. Subqueries and multi-statement queries are not permitted.
459-
routing_context (Optional[QueryRoutingOverrideContext]): Per-entity routing context
460-
for multi-folder queries. When present, included in the request body
461-
and takes precedence over the folder header on the backend.
485+
486+
Notes:
487+
A routing context is always derived from the configured ``folders_map``
488+
when present and included in the request body.
462489
463490
Returns:
464491
List[Dict[str, Any]]: A list of result records as dictionaries.
@@ -467,28 +494,24 @@ async def query_entity_records_async(
467494
ValueError: If the SQL query fails validation (e.g., non-SELECT, missing
468495
WHERE/LIMIT, forbidden keywords, subqueries).
469496
"""
470-
return await self._query_entities_for_records_async(
471-
sql_query, routing_context=routing_context
472-
)
497+
return await self._query_entities_for_records_async(sql_query)
473498

474499
def _query_entities_for_records(
475500
self,
476501
sql_query: str,
477-
*,
478-
routing_context: Optional[QueryRoutingOverrideContext] = None,
479502
) -> List[Dict[str, Any]]:
480503
self._validate_sql_query(sql_query)
504+
routing_context = self._build_routing_context_from_map()
481505
spec = self._query_entity_records_spec(sql_query, routing_context)
482506
response = self.request(spec.method, spec.endpoint, json=spec.json)
483507
return response.json().get("results", [])
484508

485509
async def _query_entities_for_records_async(
486510
self,
487511
sql_query: str,
488-
*,
489-
routing_context: Optional[QueryRoutingOverrideContext] = None,
490512
) -> List[Dict[str, Any]]:
491513
self._validate_sql_query(sql_query)
514+
routing_context = await self._build_routing_context_from_map_async()
492515
spec = self._query_entity_records_spec(sql_query, routing_context)
493516
response = await self.request_async(spec.method, spec.endpoint, json=spec.json)
494517
return response.json().get("results", [])
@@ -992,6 +1015,131 @@ def _query_entity_records_spec(
9921015
json=body,
9931016
)
9941017

1018+
def _build_routing_context_from_map(
1019+
self,
1020+
) -> Optional[QueryRoutingOverrideContext]:
1021+
"""Build a routing context from the configured folders_map and context overwrites.
1022+
1023+
Folder paths in the map are resolved to folder keys via FolderService.
1024+
Entity overwrites from the active ``ResourceOverwritesContext`` are
1025+
merged in, supplying ``override_entity_name`` when the overwrite
1026+
provides a different entity name.
1027+
1028+
Returns:
1029+
A QueryRoutingOverrideContext if routing entries exist,
1030+
None otherwise.
1031+
"""
1032+
resolved = self._resolve_folder_paths_to_ids()
1033+
return self._build_routing_context_from_resolved_map(resolved)
1034+
1035+
async def _build_routing_context_from_map_async(
1036+
self,
1037+
) -> Optional[QueryRoutingOverrideContext]:
1038+
"""Async version of _build_routing_context_from_map."""
1039+
resolved = await self._resolve_folder_paths_to_ids_async()
1040+
return self._build_routing_context_from_resolved_map(resolved)
1041+
1042+
def _resolve_folder_paths_to_ids(self) -> Optional[dict[str, str]]:
1043+
if not self._folders_map:
1044+
return None
1045+
1046+
resolved: dict[str, str] = {}
1047+
for folder_path in set(self._folders_map.values()):
1048+
if self._folders_service is not None:
1049+
folder_key = self._folders_service.retrieve_folder_key(folder_path)
1050+
if folder_key is not None:
1051+
resolved[folder_path] = folder_key
1052+
continue
1053+
resolved[folder_path] = folder_path
1054+
1055+
return resolved
1056+
1057+
async def _resolve_folder_paths_to_ids_async(self) -> Optional[dict[str, str]]:
1058+
if not self._folders_map:
1059+
return None
1060+
1061+
resolved: dict[str, str] = {}
1062+
for folder_path in set(self._folders_map.values()):
1063+
if self._folders_service is not None:
1064+
folder_key = await self._folders_service.retrieve_folder_key_async(
1065+
folder_path
1066+
)
1067+
if folder_key is not None:
1068+
resolved[folder_path] = folder_key
1069+
continue
1070+
resolved[folder_path] = folder_path
1071+
1072+
return resolved
1073+
1074+
@staticmethod
1075+
def _get_entity_overwrites_from_context() -> Dict[str, EntityResourceOverwrite]:
1076+
"""Extract entity overwrites from the active ResourceOverwritesContext.
1077+
1078+
Returns:
1079+
A dict mapping original entity name to its EntityResourceOverwrite.
1080+
"""
1081+
context_overwrites = _resource_overwrites.get()
1082+
if not context_overwrites:
1083+
return {}
1084+
1085+
result: Dict[str, EntityResourceOverwrite] = {}
1086+
for key, overwrite in context_overwrites.items():
1087+
if isinstance(overwrite, EntityResourceOverwrite):
1088+
# Key format is "entity.<original_name>"
1089+
original_name = key.split(".", 1)[1] if "." in key else key
1090+
result[original_name] = overwrite
1091+
return result
1092+
1093+
def _build_routing_context_from_resolved_map(
1094+
self,
1095+
resolved: Optional[dict[str, str]],
1096+
) -> Optional[QueryRoutingOverrideContext]:
1097+
entity_overwrites = self._get_entity_overwrites_from_context()
1098+
1099+
routings: List[EntityRouting] = []
1100+
1101+
# Add routings from folders_map
1102+
if self._folders_map and resolved is not None:
1103+
for name, folder_path in self._folders_map.items():
1104+
overwrite = entity_overwrites.pop(name, None)
1105+
override_name = (
1106+
overwrite.resource_identifier
1107+
if overwrite and overwrite.resource_identifier != name
1108+
else None
1109+
)
1110+
folder_id = (
1111+
overwrite.folder_identifier
1112+
if overwrite
1113+
else resolved.get(folder_path, folder_path)
1114+
)
1115+
routings.append(
1116+
EntityRouting(
1117+
entity_name=name,
1118+
folder_id=folder_id,
1119+
override_entity_name=override_name,
1120+
)
1121+
)
1122+
1123+
# Add routings from context overwrites not already in folders_map
1124+
for original_name, overwrite in entity_overwrites.items():
1125+
override_name = (
1126+
overwrite.resource_identifier
1127+
if overwrite.resource_identifier != original_name
1128+
else None
1129+
)
1130+
routings.append(
1131+
EntityRouting(
1132+
entity_name=original_name,
1133+
folder_id=overwrite.folder_identifier,
1134+
override_entity_name=override_name,
1135+
)
1136+
)
1137+
1138+
if not routings:
1139+
return None
1140+
1141+
return QueryRoutingOverrideContext(entity_routings=routings)
1142+
9951143
def _insert_batch_spec(self, entity_key: str, records: List[Any]) -> RequestSpec:
9961144
return RequestSpec(
9971145
method="POST",

0 commit comments

Comments
 (0)