Skip to content

Commit 3e525cc

Browse files
fix: make entity query service folder-aware with routing context (#1490)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e0a9211 commit 3e525cc

7 files changed

Lines changed: 139 additions & 11 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.11"
3+
version = "0.1.12"
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/entities/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
EntityFieldMetadata,
1111
EntityRecord,
1212
EntityRecordsBatchResponse,
13+
EntityRouting,
1314
ExternalField,
1415
ExternalObject,
1516
ExternalSourceFields,
1617
FieldDataType,
1718
FieldMetadata,
19+
QueryRoutingOverrideContext,
1820
ReferenceType,
1921
SourceJoinCriteria,
2022
)
@@ -25,12 +27,14 @@
2527
"EntityField",
2628
"EntityRecord",
2729
"EntityFieldMetadata",
30+
"EntityRouting",
2831
"FieldDataType",
2932
"FieldMetadata",
3033
"EntityRecordsBatchResponse",
3134
"ExternalField",
3235
"ExternalObject",
3336
"ExternalSourceFields",
37+
"QueryRoutingOverrideContext",
3438
"ReferenceType",
3539
"SourceJoinCriteria",
3640
]

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

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Entity,
1515
EntityRecord,
1616
EntityRecordsBatchResponse,
17+
QueryRoutingOverrideContext,
1718
)
1819

1920
_FORBIDDEN_DML = {"INSERT", "UPDATE", "DELETE", "MERGE", "REPLACE"}
@@ -416,6 +417,7 @@ class CustomerRecord:
416417
def query_entity_records(
417418
self,
418419
sql_query: str,
420+
routing_context: Optional[QueryRoutingOverrideContext] = None,
419421
) -> List[Dict[str, Any]]:
420422
"""Query entity records using a validated SQL query.
421423
@@ -425,6 +427,9 @@ def query_entity_records(
425427
sql_query (str): A SQL SELECT query to execute against Data Service entities.
426428
Only SELECT statements are allowed. Queries without WHERE must include
427429
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.
428433
429434
Returns:
430435
List[Dict[str, Any]]: A list of result records as dictionaries.
@@ -433,12 +438,15 @@ def query_entity_records(
433438
ValueError: If the SQL query fails validation (e.g., non-SELECT, missing
434439
WHERE/LIMIT, forbidden keywords, subqueries).
435440
"""
436-
return self._query_entities_for_records(sql_query)
441+
return self._query_entities_for_records(
442+
sql_query, routing_context=routing_context
443+
)
437444

438445
@traced(name="entity_query_records", run_type="uipath")
439446
async def query_entity_records_async(
440447
self,
441448
sql_query: str,
449+
routing_context: Optional[QueryRoutingOverrideContext] = None,
442450
) -> List[Dict[str, Any]]:
443451
"""Asynchronously query entity records using a validated SQL query.
444452
@@ -448,6 +456,9 @@ async def query_entity_records_async(
448456
sql_query (str): A SQL SELECT query to execute against Data Service entities.
449457
Only SELECT statements are allowed. Queries without WHERE must include
450458
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.
451462
452463
Returns:
453464
List[Dict[str, Any]]: A list of result records as dictionaries.
@@ -456,19 +467,29 @@ async def query_entity_records_async(
456467
ValueError: If the SQL query fails validation (e.g., non-SELECT, missing
457468
WHERE/LIMIT, forbidden keywords, subqueries).
458469
"""
459-
return await self._query_entities_for_records_async(sql_query)
470+
return await self._query_entities_for_records_async(
471+
sql_query, routing_context=routing_context
472+
)
460473

461-
def _query_entities_for_records(self, sql_query: str) -> List[Dict[str, Any]]:
474+
def _query_entities_for_records(
475+
self,
476+
sql_query: str,
477+
*,
478+
routing_context: Optional[QueryRoutingOverrideContext] = None,
479+
) -> List[Dict[str, Any]]:
462480
self._validate_sql_query(sql_query)
463-
spec = self._query_entity_records_spec(sql_query)
481+
spec = self._query_entity_records_spec(sql_query, routing_context)
464482
response = self.request(spec.method, spec.endpoint, json=spec.json)
465483
return response.json().get("results", [])
466484

467485
async def _query_entities_for_records_async(
468-
self, sql_query: str
486+
self,
487+
sql_query: str,
488+
*,
489+
routing_context: Optional[QueryRoutingOverrideContext] = None,
469490
) -> List[Dict[str, Any]]:
470491
self._validate_sql_query(sql_query)
471-
spec = self._query_entity_records_spec(sql_query)
492+
spec = self._query_entity_records_spec(sql_query, routing_context)
472493
response = await self.request_async(spec.method, spec.endpoint, json=spec.json)
473494
return response.json().get("results", [])
474495

@@ -958,11 +979,17 @@ def _list_records_spec(
958979
def _query_entity_records_spec(
959980
self,
960981
sql_query: str,
982+
routing_context: Optional[QueryRoutingOverrideContext] = None,
961983
) -> RequestSpec:
984+
body: Dict[str, Any] = {"query": sql_query}
985+
if routing_context:
986+
body["routingContext"] = routing_context.model_dump(
987+
by_alias=True, exclude_none=True
988+
)
962989
return RequestSpec(
963990
method="POST",
964991
endpoint=Endpoint("datafabric_/api/v1/query/execute"),
965-
json={"query": sql_query},
992+
json=body,
966993
)
967994

968995
def _insert_batch_spec(self, entity_key: str, records: List[Any]) -> RequestSpec:

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,4 +322,24 @@ class EntityRecordsBatchResponse(BaseModel):
322322
failure_records: List[EntityRecord] = Field(alias="failureRecords")
323323

324324

325+
class EntityRouting(BaseModel):
326+
"""A single entity-to-folder routing entry for query execution."""
327+
328+
model_config = ConfigDict(populate_by_name=True)
329+
330+
entity_name: str = Field(alias="entityName")
331+
folder_id: str = Field(alias="folderId")
332+
override_entity_name: Optional[str] = Field(
333+
default=None, alias="overrideEntityName"
334+
)
335+
336+
337+
class QueryRoutingOverrideContext(BaseModel):
338+
"""Routing context that maps entities to their folders for multi-entity queries."""
339+
340+
model_config = ConfigDict(populate_by_name=True)
341+
342+
entity_routings: List[EntityRouting] = Field(alias="entityRoutings")
343+
344+
325345
Entity.model_rebuild()

packages/uipath-platform/tests/services/test_entities_service.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pytest_httpx import HTTPXMock
99

1010
from uipath.platform import UiPathApiConfig, UiPathExecutionContext
11-
from uipath.platform.entities import Entity
11+
from uipath.platform.entities import Entity, EntityRouting, QueryRoutingOverrideContext
1212
from uipath.platform.entities._entities_service import EntitiesService
1313

1414

@@ -389,3 +389,80 @@ async def test_query_entity_records_async_calls_request_for_valid_sql(
389389

390390
assert result == [{"id": "c1"}]
391391
service.request_async.assert_called_once()
392+
393+
def test_query_entity_records_with_routing_context(
394+
self,
395+
service: EntitiesService,
396+
) -> None:
397+
response = MagicMock()
398+
response.json.return_value = {"results": [{"id": 1}]}
399+
service.request = MagicMock(return_value=response) # type: ignore[method-assign]
400+
401+
routing = QueryRoutingOverrideContext(
402+
entity_routings=[
403+
EntityRouting(entity_name="Customers", folder_id="folder-1"),
404+
EntityRouting(
405+
entity_name="Orders",
406+
folder_id="folder-2",
407+
override_entity_name="OrdersV2",
408+
),
409+
]
410+
)
411+
412+
result = service.query_entity_records(
413+
"SELECT id FROM Customers LIMIT 10", routing_context=routing
414+
)
415+
416+
assert result == [{"id": 1}]
417+
call_kwargs = service.request.call_args
418+
body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json")
419+
assert body["query"] == "SELECT id FROM Customers LIMIT 10"
420+
assert body["routingContext"] == {
421+
"entityRoutings": [
422+
{"entityName": "Customers", "folderId": "folder-1"},
423+
{
424+
"entityName": "Orders",
425+
"folderId": "folder-2",
426+
"overrideEntityName": "OrdersV2",
427+
},
428+
]
429+
}
430+
431+
@pytest.mark.anyio
432+
async def test_query_entity_records_async_with_routing_context(
433+
self,
434+
service: EntitiesService,
435+
) -> None:
436+
response = MagicMock()
437+
response.json.return_value = {"results": [{"id": "c1"}]}
438+
service.request_async = AsyncMock(return_value=response) # type: ignore[method-assign]
439+
440+
routing = QueryRoutingOverrideContext(
441+
entity_routings=[
442+
EntityRouting(entity_name="Customers", folder_id="folder-1"),
443+
]
444+
)
445+
446+
result = await service.query_entity_records_async(
447+
"SELECT id FROM Customers WHERE id = 'c1'",
448+
routing_context=routing,
449+
)
450+
451+
assert result == [{"id": "c1"}]
452+
call_kwargs = service.request_async.call_args
453+
body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json")
454+
assert "routingContext" in body
455+
456+
def test_query_entity_records_without_routing_context_omits_key(
457+
self,
458+
service: EntitiesService,
459+
) -> None:
460+
response = MagicMock()
461+
response.json.return_value = {"results": []}
462+
service.request = MagicMock(return_value=response) # type: ignore[method-assign]
463+
464+
service.query_entity_records("SELECT id FROM Customers WHERE id > 0")
465+
466+
call_kwargs = service.request.call_args
467+
body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json")
468+
assert "routingContext" not in body

packages/uipath-platform/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/uipath/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)