Skip to content

Commit d4e7a38

Browse files
authored
feat: ecs batchrag - download encrypted artifacts using auth headers (#1249)
1 parent 2216376 commit d4e7a38

5 files changed

Lines changed: 167 additions & 6 deletions

File tree

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"
3-
version = "2.6.29"
3+
version = "2.6.30"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/platform/context_grounding/_context_grounding_service.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -782,9 +782,20 @@ def download_batch_transform_result(
782782

783783
Path(destination_path).parent.mkdir(parents=True, exist_ok=True)
784784

785+
# SAS uris can be downloaded without authentication
786+
# encrypted artifacts require authenticated DownloadBlob endpoint
785787
with open(destination_path, "wb") as file:
786-
with httpx.Client(**get_httpx_client_kwargs()) as client:
787-
file_content = client.get(uri_response.uri).content
788+
if uri_response.is_encrypted:
789+
download_spec = self._batch_transform_download_blob_spec(id=id)
790+
download_response = self.request(
791+
download_spec.method,
792+
download_spec.endpoint,
793+
headers=download_spec.headers,
794+
)
795+
file_content = download_response.content
796+
else:
797+
with httpx.Client(**get_httpx_client_kwargs()) as client:
798+
file_content = client.get(uri_response.uri).content
788799
file.write(file_content)
789800

790801
@resource_override(resource_type="index", resource_identifier="index_name")
@@ -828,9 +839,20 @@ async def download_batch_transform_result_async(
828839
)
829840
uri_response = BatchTransformReadUriResponse.model_validate(response.json())
830841

831-
async with httpx.AsyncClient(**get_httpx_client_kwargs()) as client:
832-
download_response = await client.get(uri_response.uri)
842+
# SAS uris can be downloaded without authentication
843+
# encrypted artifacts require authenticated DownloadBlob endpoint
844+
if uri_response.is_encrypted:
845+
download_spec = self._batch_transform_download_blob_spec(id=id)
846+
download_response = await self.request_async(
847+
download_spec.method,
848+
download_spec.endpoint,
849+
headers=download_spec.headers,
850+
)
833851
file_content = download_response.content
852+
else:
853+
async with httpx.AsyncClient(**get_httpx_client_kwargs()) as client:
854+
download_response = await client.get(uri_response.uri)
855+
file_content = download_response.content
834856

835857
Path(destination_path).parent.mkdir(parents=True, exist_ok=True)
836858

@@ -1515,6 +1537,15 @@ def _batch_transform_get_read_uri_spec(
15151537
endpoint=Endpoint(f"/ecs_/v2/batchRag/{id}/GetReadUri"),
15161538
)
15171539

1540+
def _batch_transform_download_blob_spec(
1541+
self,
1542+
id: str,
1543+
) -> RequestSpec:
1544+
return RequestSpec(
1545+
method="GET",
1546+
endpoint=Endpoint(f"/ecs_/v2/batchRag/{id}/DownloadBlob"),
1547+
)
1548+
15181549
def _resolve_folder_key(self, folder_key, folder_path):
15191550
if folder_key is None and folder_path is not None:
15201551
folder_key = self._folders_service.retrieve_key(folder_path=folder_path)

src/uipath/platform/context_grounding/context_grounding.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ class BatchTransformReadUriResponse(BaseModel):
157157
arbitrary_types_allowed=True,
158158
)
159159
uri: str
160+
is_encrypted: bool = Field(alias="isEncrypted", default=False)
160161

161162

162163
class DeepRagCreationResponse(BaseModel):

tests/sdk/services/test_context_grounding_service.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,6 +1597,7 @@ def test_download_batch_transform_result(
15971597
status_code=200,
15981598
json={
15991599
"uri": "https://storage.example.com/result.csv",
1600+
"isEncrypted": False,
16001601
},
16011602
)
16021603

@@ -1670,6 +1671,7 @@ async def test_download_batch_transform_result_async(
16701671
status_code=200,
16711672
json={
16721673
"uri": "https://storage.example.com/result.csv",
1674+
"isEncrypted": False,
16731675
},
16741676
)
16751677

@@ -1741,6 +1743,7 @@ def test_download_batch_transform_result_creates_nested_directories(
17411743
status_code=200,
17421744
json={
17431745
"uri": "https://storage.example.com/result.csv",
1746+
"isEncrypted": False,
17441747
},
17451748
)
17461749

@@ -1760,6 +1763,68 @@ def test_download_batch_transform_result_creates_nested_directories(
17601763
assert destination.read_bytes() == b"col1,col2\nval1,val2"
17611764
assert destination.parent.exists()
17621765

1766+
def test_download_batch_transform_result_encrypted(
1767+
self,
1768+
httpx_mock: HTTPXMock,
1769+
service: ContextGroundingService,
1770+
base_url: str,
1771+
org: str,
1772+
tenant: str,
1773+
version: str,
1774+
tmp_path,
1775+
) -> None:
1776+
httpx_mock.add_response(
1777+
url=f"{base_url}{org}{tenant}/ecs_/v2/batchRag/test-batch-id",
1778+
status_code=200,
1779+
json={
1780+
"id": "test-batch-id",
1781+
"name": "test-batch-transform",
1782+
"lastBatchRagStatus": "Successful",
1783+
"prompt": "Summarize documents",
1784+
"targetFileGlobPattern": "**",
1785+
"useWebSearchGrounding": False,
1786+
"outputColumns": [
1787+
{"name": "summary", "description": "Document summary"}
1788+
],
1789+
"createdDate": "2024-01-15T10:30:00Z",
1790+
},
1791+
)
1792+
1793+
httpx_mock.add_response(
1794+
url=f"{base_url}{org}{tenant}/ecs_/v2/batchRag/test-batch-id/GetReadUri",
1795+
status_code=200,
1796+
json={
1797+
"uri": f"{base_url}{org}{tenant}/ecs_/v2/batchRag/test-batch-id/DownloadBlob",
1798+
"isEncrypted": True,
1799+
},
1800+
)
1801+
1802+
httpx_mock.add_response(
1803+
url=f"{base_url}{org}{tenant}/ecs_/v2/batchRag/test-batch-id/DownloadBlob",
1804+
status_code=200,
1805+
content=b"encrypted,data\nval1,val2",
1806+
)
1807+
1808+
destination = tmp_path / "result_encrypted.csv"
1809+
service.download_batch_transform_result(
1810+
id="test-batch-id",
1811+
destination_path=str(destination),
1812+
)
1813+
1814+
assert destination.exists()
1815+
assert destination.read_bytes() == b"encrypted,data\nval1,val2"
1816+
1817+
sent_requests = httpx_mock.get_requests()
1818+
if sent_requests is None:
1819+
raise Exception("No request was sent")
1820+
1821+
# Verify the DownloadBlob endpoint was called with Authorization header
1822+
download_request = sent_requests[2]
1823+
assert download_request.method == "GET"
1824+
assert "/DownloadBlob" in str(download_request.url)
1825+
assert "Authorization" in download_request.headers
1826+
assert download_request.headers["Authorization"].startswith("Bearer ")
1827+
17631828
def test_create_ephemeral_index(
17641829
self,
17651830
httpx_mock: HTTPXMock,
@@ -1903,6 +1968,7 @@ async def test_download_batch_transform_result_async_creates_nested_directories(
19031968
status_code=200,
19041969
json={
19051970
"uri": "https://storage.example.com/result.csv",
1971+
"isEncrypted": False,
19061972
},
19071973
)
19081974

@@ -1921,3 +1987,66 @@ async def test_download_batch_transform_result_async_creates_nested_directories(
19211987
assert destination.exists()
19221988
assert destination.read_bytes() == b"col1,col2\nval1,val2"
19231989
assert destination.parent.exists()
1990+
1991+
@pytest.mark.anyio
1992+
async def test_download_batch_transform_result_async_encrypted(
1993+
self,
1994+
httpx_mock: HTTPXMock,
1995+
service: ContextGroundingService,
1996+
base_url: str,
1997+
org: str,
1998+
tenant: str,
1999+
version: str,
2000+
tmp_path,
2001+
) -> None:
2002+
httpx_mock.add_response(
2003+
url=f"{base_url}{org}{tenant}/ecs_/v2/batchRag/test-batch-id",
2004+
status_code=200,
2005+
json={
2006+
"id": "test-batch-id",
2007+
"name": "test-batch-transform",
2008+
"lastBatchRagStatus": "Successful",
2009+
"prompt": "Summarize documents",
2010+
"targetFileGlobPattern": "**",
2011+
"useWebSearchGrounding": False,
2012+
"outputColumns": [
2013+
{"name": "summary", "description": "Document summary"}
2014+
],
2015+
"createdDate": "2024-01-15T10:30:00Z",
2016+
},
2017+
)
2018+
2019+
httpx_mock.add_response(
2020+
url=f"{base_url}{org}{tenant}/ecs_/v2/batchRag/test-batch-id/GetReadUri",
2021+
status_code=200,
2022+
json={
2023+
"uri": f"{base_url}{org}{tenant}/ecs_/v2/batchRag/test-batch-id/DownloadBlob",
2024+
"isEncrypted": True,
2025+
},
2026+
)
2027+
2028+
httpx_mock.add_response(
2029+
url=f"{base_url}{org}{tenant}/ecs_/v2/batchRag/test-batch-id/DownloadBlob",
2030+
status_code=200,
2031+
content=b"encrypted,data\nval1,val2",
2032+
)
2033+
2034+
destination = tmp_path / "result_encrypted.csv"
2035+
await service.download_batch_transform_result_async(
2036+
id="test-batch-id",
2037+
destination_path=str(destination),
2038+
)
2039+
2040+
assert destination.exists()
2041+
assert destination.read_bytes() == b"encrypted,data\nval1,val2"
2042+
2043+
sent_requests = httpx_mock.get_requests()
2044+
if sent_requests is None:
2045+
raise Exception("No request was sent")
2046+
2047+
# Verify the DownloadBlob endpoint was called with Authorization header
2048+
download_request = sent_requests[2]
2049+
assert download_request.method == "GET"
2050+
assert "/DownloadBlob" in str(download_request.url)
2051+
assert "Authorization" in download_request.headers
2052+
assert download_request.headers["Authorization"].startswith("Bearer ")

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)