Skip to content

Commit 9965d6e

Browse files
authored
fix(platform): close service http clients (#1826)
1 parent 684009f commit 9965d6e

11 files changed

Lines changed: 95 additions & 3 deletions

File tree

packages/uipath-platform/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
[project]
22
name = "uipath-platform"
3-
version = "0.2.12"
3+
version = "0.2.13"
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"
77
dependencies = [
8+
"anyio>=4.0.0",
89
"httpx>=0.28.1",
910
"tenacity>=9.0.0",
1011
"truststore>=0.10.1",

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from logging import getLogger
44
from typing import Any, Literal, Union
55

6+
from anyio import to_thread
67
from httpx import (
78
URL,
89
AsyncClient,
@@ -171,6 +172,18 @@ def __init__(
171172

172173
super().__init__()
173174

175+
async def aclose(self) -> None:
176+
"""Close the HTTP clients owned by this service.
177+
178+
The asynchronous client is closed by the active async backend. Closing the
179+
synchronous client can block while its transport is torn down, so that work
180+
runs in a backend-neutral worker thread.
181+
"""
182+
try:
183+
await self._client_async.aclose()
184+
finally:
185+
await to_thread.run_sync(self._client.close)
186+
174187
@retry(
175188
retry=(
176189
retry_if_exception(is_retryable_platform_exception)

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,19 @@ def __init__(
112112
folders_service=folders_service,
113113
)
114114

115+
async def aclose(self) -> None:
116+
"""Close this facade and the services it creates."""
117+
try:
118+
await self._schema.aclose()
119+
finally:
120+
try:
121+
await self._data.aclose()
122+
finally:
123+
try:
124+
await self._ontology.aclose()
125+
finally:
126+
await super().aclose()
127+
115128
# ------------------------------------------------------------------
116129
# Schema operations — delegate to EntitySchemaService
117130
# ------------------------------------------------------------------

packages/uipath-platform/src/uipath/platform/orchestrator/_buckets_service.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Any, Dict, Optional, Union
66

77
import httpx
8+
from anyio import to_thread
89
from uipath.core.tracing import traced
910

1011
from ..common._base_service import BaseService
@@ -37,6 +38,16 @@ def __init__(
3738
self.custom_client = httpx.Client(**get_httpx_client_kwargs())
3839
self.custom_client_async = httpx.AsyncClient(**get_httpx_client_kwargs())
3940

41+
async def aclose(self) -> None:
42+
"""Close the additional HTTP clients used for bucket transfers."""
43+
try:
44+
await self.custom_client_async.aclose()
45+
finally:
46+
try:
47+
await to_thread.run_sync(self.custom_client.close)
48+
finally:
49+
await super().aclose()
50+
4051
@traced(name="buckets_list", run_type="uipath")
4152
def list(
4253
self,

packages/uipath-platform/src/uipath/platform/orchestrator/_jobs_service.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ def __init__(
4343
self._temp_dir = os.path.join(tempfile.gettempdir(), TEMP_ATTACHMENTS_FOLDER)
4444
os.makedirs(self._temp_dir, exist_ok=True)
4545

46+
async def aclose(self) -> None:
47+
"""Close this service and the attachment service it owns."""
48+
try:
49+
await self._attachments_service.aclose()
50+
finally:
51+
await super().aclose()
52+
4653
@overload
4754
def resume(self, *, inbox_id: str, payload: Any) -> None: ...
4855

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ class TestBaseService:
1818
def test_init_base_service(self, service: BaseService):
1919
assert service is not None
2020

21+
@pytest.mark.parametrize("anyio_backend", ["asyncio", "trio"])
22+
@pytest.mark.anyio
23+
async def test_aclose_closes_owned_http_clients(self, service: BaseService):
24+
await service.aclose()
25+
26+
assert service._client.is_closed
27+
assert service._client_async.is_closed
28+
2129
def test_base_service_default_headers(self, service: BaseService, secret: str):
2230
assert service.default_headers == {
2331
"Accept": "application/json",

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ def temp_file(tmp_path):
2727

2828

2929
class TestBucketsService:
30+
@pytest.mark.parametrize("anyio_backend", ["asyncio", "trio"])
31+
@pytest.mark.anyio
32+
async def test_aclose_closes_all_owned_http_clients(
33+
self, service: BucketsService
34+
) -> None:
35+
await service.aclose()
36+
37+
assert service._client.is_closed
38+
assert service._client_async.is_closed
39+
assert service.custom_client.is_closed
40+
assert service.custom_client_async.is_closed
41+
3042
class TestRetrieve:
3143
def test_retrieve_by_key(
3244
self,

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ def record_schema_optional(request):
5555

5656

5757
class TestEntitiesService:
58+
@pytest.mark.parametrize("anyio_backend", ["asyncio", "trio"])
59+
@pytest.mark.anyio
60+
async def test_aclose_closes_owned_services(self, service: EntitiesService):
61+
await service.aclose()
62+
63+
assert service._client.is_closed
64+
assert service._client_async.is_closed
65+
assert service._schema._client.is_closed
66+
assert service._schema._client_async.is_closed
67+
assert service._data._client.is_closed
68+
assert service._data._client_async.is_closed
69+
assert service._ontology._client.is_closed
70+
assert service._ontology._client_async.is_closed
71+
5872
def test_query_entity_records_has_datafabric_error_mapping(self) -> None:
5973
assert (
6074
EntitiesService.query_entity_records.__uipath_datafabric_method__ # type: ignore[attr-defined]

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ def local_attachment_file(
101101

102102

103103
class TestJobsService:
104+
@pytest.mark.anyio
105+
async def test_aclose_closes_owned_services(self, service: JobsService):
106+
await service.aclose()
107+
108+
assert service._client.is_closed
109+
assert service._client_async.is_closed
110+
assert service._attachments_service._client.is_closed
111+
assert service._attachments_service._client_async.is_closed
112+
104113
def test_retrieve(
105114
self,
106115
httpx_mock: HTTPXMock,

packages/uipath-platform/uv.lock

Lines changed: 3 additions & 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)