Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-platform"
version = "0.2.7"
version = "0.2.8"
description = "HTTP client library for programmatic access to UiPath Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
HEADER_FOLDER_PATH = "x-uipath-folderpath"
HEADER_FOLDER_PATH_ENCODED = "x-uipath-folderpath-encoded"
HEADER_USER_AGENT = "x-uipath-user-agent"
HEADER_SOURCE = "x-uipath-source"
HEADER_TENANT_ID = "x-uipath-tenantid"
HEADER_INTERNAL_TENANT_ID = "x-uipath-internal-tenantid"
HEADER_INTERNAL_ACCOUNT_ID = "x-uipath-internal-accountid"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1820,7 +1820,9 @@ async def retrieve_records_async(

@attach_datafabric_error_mapping("query_entity_records")
@traced(name="entity_query_records", run_type="uipath")
def query_entity_records(self, sql_query: str) -> List[Dict[str, Any]]:
def query_entity_records(
self, sql_query: str, source: Optional[str] = None
) -> List[Dict[str, Any]]:
"""Query entity records using a validated SQL query.

PREVIEW: This method is in preview and may change in future releases.
Expand All @@ -1829,6 +1831,9 @@ def query_entity_records(self, sql_query: str) -> List[Dict[str, Any]]:
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.
source (str, optional): Value for the ``x-uipath-source`` header on the
query/execute request, identifying the calling surface (e.g.
``"LOW_CODE_AGENT"``). Omitted from the request when not set.

Notes:
A routing context is always derived from the configured ``folders_map``
Expand All @@ -1841,11 +1846,13 @@ def query_entity_records(self, sql_query: str) -> List[Dict[str, Any]]:
ValueError: If the SQL query fails validation (e.g., non-SELECT, missing
WHERE/LIMIT, forbidden keywords, subqueries).
"""
return self._data.query_entity_records(sql_query)
return self._data.query_entity_records(sql_query, source)

@attach_datafabric_error_mapping("query_entity_records_async")
@traced(name="entity_query_records", run_type="uipath")
async def query_entity_records_async(self, sql_query: str) -> List[Dict[str, Any]]:
async def query_entity_records_async(
self, sql_query: str, source: Optional[str] = None
) -> List[Dict[str, Any]]:
"""Asynchronously query entity records using a validated SQL query.

PREVIEW: This method is in preview and may change in future releases.
Expand All @@ -1854,6 +1861,9 @@ async def query_entity_records_async(self, sql_query: str) -> List[Dict[str, Any
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.
source (str, optional): Value for the ``x-uipath-source`` header on the
query/execute request, identifying the calling surface (e.g.
``"LOW_CODE_AGENT"``). Omitted from the request when not set.

Notes:
A routing context is always derived from the configured ``folders_map``
Expand All @@ -1866,7 +1876,7 @@ async def query_entity_records_async(self, sql_query: str) -> List[Dict[str, Any
ValueError: If the SQL query fails validation (e.g., non-SELECT, missing
WHERE/LIMIT, forbidden keywords, subqueries).
"""
return await self._data.query_entity_records_async(sql_query)
return await self._data.query_entity_records_async(sql_query, source)

@traced(name="entity_upload_attachment", run_type="uipath")
def upload_attachment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ..common._config import UiPathApiConfig
from ..common._execution_context import UiPathExecutionContext
from ..common._models import Endpoint, RequestSpec
from ..constants import HEADER_SOURCE
from ..errors._enriched_exception import EnrichedException
from ..orchestrator._folder_service import FolderService
from ._entity_resolution import RoutingStrategy, create_routing_strategy
Expand Down Expand Up @@ -518,16 +519,18 @@ async def retrieve_records_async(
def query_entity_records(
self,
sql_query: str,
source: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""Internal implementation; see :meth:`EntitiesService.query_entity_records`."""
return self._query_entities_for_records(sql_query)
return self._query_entities_for_records(sql_query, source)

async def query_entity_records_async(
self,
sql_query: str,
source: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""Async variant of :meth:`query_entity_records`."""
return await self._query_entities_for_records_async(sql_query)
return await self._query_entities_for_records_async(sql_query, source)

# ------------------------------------------------------------------
# Attachments
Expand Down Expand Up @@ -680,22 +683,28 @@ def validate_entity_batch(
# Internal helpers — request specs
# ------------------------------------------------------------------

def _query_entities_for_records(self, sql_query: str) -> List[Dict[str, Any]]:
def _query_entities_for_records(
self, sql_query: str, source: Optional[str] = None
) -> List[Dict[str, Any]]:
"""Synchronously run a validated SQL query through the federated query engine."""
self._validate_sql_query(sql_query)
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)
spec = self._query_entity_records_spec(sql_query, routing_context, source)
response = self.request(
spec.method, spec.endpoint, json=spec.json, headers=spec.headers
)
return response.json().get("results", [])

async def _query_entities_for_records_async(
self, sql_query: str
self, sql_query: str, source: Optional[str] = None
) -> List[Dict[str, Any]]:
"""Asynchronously run a validated SQL query through the federated query engine."""
self._validate_sql_query(sql_query)
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)
spec = self._query_entity_records_spec(sql_query, routing_context, source)
response = await self.request_async(
spec.method, spec.endpoint, json=spec.json, headers=spec.headers
)
return response.json().get("results", [])

@staticmethod
Expand Down Expand Up @@ -956,17 +965,20 @@ def _retrieve_records_spec(
def _query_entity_records_spec(
sql_query: str,
routing_context: Optional[QueryRoutingOverrideContext] = None,
source: Optional[str] = None,
) -> RequestSpec:
"""Build the POST spec for the federated SQL query endpoint."""
body: Dict[str, Any] = {"query": sql_query}
if routing_context:
body["routingContext"] = routing_context.model_dump(
by_alias=True, exclude_none=True
)
headers = {HEADER_SOURCE: source} if source else {}
return RequestSpec(
method="POST",
endpoint=Endpoint("datafabric_/api/v1/query/execute"),
json=body,
headers=headers,
)

@staticmethod
Expand Down
28 changes: 28 additions & 0 deletions packages/uipath-platform/tests/services/test_entities_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@
assert records[1].integer_field == 11
else:
# Validation should fail and raise an exception
with pytest.raises((ValueError, TypeError)):

Check warning on line 221 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLA&open=AZ9aoYXs1JCbMyYXxTLA&pullRequest=1808
service.list_records(
entity_key=str(entity_key), schema=RecordSchema, start=0, limit=1
)
Expand Down Expand Up @@ -275,7 +275,7 @@
assert records[1].name == "record_name2"
else:
# Validation should fail and raise an exception for missing required field
with pytest.raises((ValueError, TypeError)):

Check warning on line 278 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLB&open=AZ9aoYXs1JCbMyYXxTLB&pullRequest=1808
service.list_records(
entity_key=str(entity_key),
schema=RecordSchemaOptional,
Expand Down Expand Up @@ -459,6 +459,34 @@
assert result == [{"id": 1}, {"id": 2}]
service._data.request.assert_called_once()

def test_query_entity_records_sets_source_header_when_provided(
self,
service: EntitiesService,
) -> None:
response = MagicMock()
response.json.return_value = {"results": []}
service._data.request = MagicMock(return_value=response) # type: ignore[method-assign]

service.query_entity_records(
"SELECT id FROM Customers WHERE id > 0", source="LOW_CODE_AGENT"
)

headers = service._data.request.call_args.kwargs.get("headers") or {}
assert headers.get("x-uipath-source") == "LOW_CODE_AGENT"

def test_query_entity_records_omits_source_header_by_default(
self,
service: EntitiesService,
) -> None:
response = MagicMock()
response.json.return_value = {"results": []}
service._data.request = MagicMock(return_value=response) # type: ignore[method-assign]

service.query_entity_records("SELECT id FROM Customers WHERE id > 0")

headers = service._data.request.call_args.kwargs.get("headers") or {}
assert "x-uipath-source" not in headers

@pytest.mark.anyio
async def test_query_entity_records_async_rejects_invalid_sql_before_network_call(
Comment on lines 490 to 491
self,
Expand Down Expand Up @@ -1913,7 +1941,7 @@
def test_create_entity_rejects_invalid_field_name(self, service) -> None:
from uipath.platform.entities import EntityCreateFieldOptions

with pytest.raises(ValueError, match="Invalid field name"):

Check warning on line 1944 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLC&open=AZ9aoYXs1JCbMyYXxTLC&pullRequest=1808
service.create_entity(
name="goodEntity",
fields=[EntityCreateFieldOptions(field_name="9bad")],
Expand All @@ -1922,7 +1950,7 @@
def test_create_entity_rejects_reserved_field_name(self, service) -> None:
from uipath.platform.entities import EntityCreateFieldOptions

with pytest.raises(ValueError, match="reserved"):

Check warning on line 1953 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLD&open=AZ9aoYXs1JCbMyYXxTLD&pullRequest=1808
service.create_entity(
name="goodEntity",
fields=[EntityCreateFieldOptions(field_name="Id")],
Expand All @@ -1936,7 +1964,7 @@
EntityFieldDataType,
)

with pytest.raises(ValueError, match="does not accept"):

Check warning on line 1967 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLE&open=AZ9aoYXs1JCbMyYXxTLE&pullRequest=1808
service.create_entity(
name="goodEntity",
fields=[
Expand All @@ -1954,7 +1982,7 @@
EntityFieldDataType,
)

with pytest.raises(ValueError, match="out of range"):

Check warning on line 1985 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLF&open=AZ9aoYXs1JCbMyYXxTLF&pullRequest=1808
service.create_entity(
name="goodEntity",
fields=[
Expand All @@ -1972,7 +2000,7 @@
EntityFieldDataType,
)

with pytest.raises(ValueError, match="strictly less than"):

Check warning on line 2003 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLG&open=AZ9aoYXs1JCbMyYXxTLG&pullRequest=1808
service.create_entity(
name="goodEntity",
fields=[
Expand All @@ -1993,7 +2021,7 @@
EntityFieldDataType,
)

with pytest.raises(ValueError, match="choice_set_id"):

Check warning on line 2024 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLH&open=AZ9aoYXs1JCbMyYXxTLH&pullRequest=1808
service.create_entity(
name="goodEntity",
fields=[
Expand All @@ -2012,7 +2040,7 @@
EntityFieldDataType,
)

with pytest.raises(ValueError, match="choice_set_id"):

Check warning on line 2043 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLI&open=AZ9aoYXs1JCbMyYXxTLI&pullRequest=1808
service.create_entity(
name="goodEntity",
fields=[
Expand All @@ -2031,7 +2059,7 @@
EntityFieldDataType,
)

with pytest.raises(ValueError, match="reference_entity_name"):

Check warning on line 2062 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLJ&open=AZ9aoYXs1JCbMyYXxTLJ&pullRequest=1808
service.create_entity(
name="goodEntity",
fields=[
Expand Down Expand Up @@ -2289,7 +2317,7 @@
)
from uipath.platform.errors._enriched_exception import EnrichedException

with pytest.raises(EnrichedException):

Check warning on line 2320 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLK&open=AZ9aoYXs1JCbMyYXxTLK&pullRequest=1808
service.update_records(
entity_key=str(entity_key), records=[{"Id": "x", "name": "y"}]
)
Expand All @@ -2307,7 +2335,7 @@
)
from uipath.platform.errors._enriched_exception import EnrichedException

with pytest.raises(EnrichedException):

Check warning on line 2338 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLL&open=AZ9aoYXs1JCbMyYXxTLL&pullRequest=1808
service.update_records(
entity_key=str(entity_key), records=[{"Id": "x", "name": "y"}]
)
Expand All @@ -2326,7 +2354,7 @@
)
from uipath.platform.errors._enriched_exception import EnrichedException

with pytest.raises(EnrichedException):

Check warning on line 2357 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLM&open=AZ9aoYXs1JCbMyYXxTLM&pullRequest=1808
service.update_records(
entity_key=str(entity_key), records=[{"Id": "x", "name": "y"}]
)
Expand Down Expand Up @@ -2656,7 +2684,7 @@
status_code=500,
json={"successRecords": [], "failureRecords": []},
)
with pytest.raises(EnrichedException):

Check warning on line 2687 in packages/uipath-platform/tests/services/test_entities_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9aoYXs1JCbMyYXxTLN&open=AZ9aoYXs1JCbMyYXxTLN&pullRequest=1808
service.insert_records(
entity_key=str(entity_key),
records=[{"name": "x"}],
Expand Down
2 changes: 1 addition & 1 deletion packages/uipath-platform/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/uipath/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading