Skip to content

Commit fa84d39

Browse files
refactor(business-rules): share run() and run_async()'s steps
The two methods repeated the same body. What they share now lives in one place, and only the folder lookup and the request differ: - _prepare_run() applies a binding, validates the arguments and picks the folder, returning a _RunTarget: the rule name, and a folder key or a path still to look up. - _resolve_folder_key() / _resolve_folder_key_async() take that target. - _request_options() builds the request() arguments, and _to_run_result() parses the response body. The request() call itself stays in run() and run_async(): BaseService names the x-uipath-user-agent after the method that calls it, so moving it into a helper would report "_send" instead of the public method. A new test pins run_async's user agent, as one already did for run's. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2c8bcb0 commit fa84d39

2 files changed

Lines changed: 88 additions & 46 deletions

File tree

‎packages/uipath-platform/src/uipath/platform/business_rules/_business_rules_service.py‎

Lines changed: 70 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import unicodedata
77
from collections.abc import Mapping
8+
from dataclasses import dataclass
89
from typing import Any, Dict, List, Optional, Tuple
910

1011
from ..common._base_service import _TRACE_PARENT_HEADER, BaseService
@@ -32,6 +33,19 @@
3233
_FORBIDDEN_RULE_NAME_PARTS = ("/", "\\", "..", "%")
3334

3435

36+
@dataclass(frozen=True)
37+
class _RunTarget:
38+
"""Where a run goes, settled before anything is sent.
39+
40+
Exactly one of ``folder_key`` and ``folder_path`` is set: a key ready to
41+
send, or a path still to look up.
42+
"""
43+
44+
name: str
45+
folder_key: Optional[str]
46+
folder_path: Optional[str]
47+
48+
3549
class BusinessRulesService(FolderContext, BaseService):
3650
"""Service for running UiPath Business Rules.
3751
@@ -108,27 +122,19 @@ def run(
108122
print(decision.decision_name, decision.outputs)
109123
```
110124
"""
111-
name, folder_key, folder_path = self._apply_binding(
112-
name, folder_key, folder_path
113-
)
114-
_validate_run_arguments(name, input)
115-
resolved_folder_key = self._resolve_folder_key(folder_key, folder_path)
125+
target = self._prepare_run(name, input, folder_key, folder_path)
116126
spec = self._evaluate_spec(
117-
name,
127+
target.name,
118128
input,
119-
folder_key=resolved_folder_key,
129+
folder_key=self._resolve_folder_key(target),
120130
version=version,
121131
decision_names=decision_names,
122132
caller=caller,
123133
)
124-
response = self.request(
125-
spec.method,
126-
url=spec.endpoint,
127-
json=spec.json,
128-
headers=_headers_with_trace(spec.headers, trace_context),
129-
scoped="tenant",
130-
)
131-
return _to_run_result(_WireResponse.model_validate(response.json()))
134+
# Called here, not in a helper: BaseService names the user agent after
135+
# the method that calls request(), which must be the public run().
136+
response = self.request(spec.method, **_request_options(spec, trace_context))
137+
return _to_run_result(response.json())
132138

133139
async def run_async(
134140
self,
@@ -165,29 +171,38 @@ async def run_async(
165171
ValueError: If the request is invalid or a required folder is missing.
166172
EnrichedException: If the service rejects the request.
167173
"""
168-
name, folder_key, folder_path = self._apply_binding(
169-
name, folder_key, folder_path
170-
)
171-
_validate_run_arguments(name, input)
172-
resolved_folder_key = await self._resolve_folder_key_async(
173-
folder_key, folder_path
174-
)
174+
target = self._prepare_run(name, input, folder_key, folder_path)
175175
spec = self._evaluate_spec(
176-
name,
176+
target.name,
177177
input,
178-
folder_key=resolved_folder_key,
178+
folder_key=await self._resolve_folder_key_async(target),
179179
version=version,
180180
decision_names=decision_names,
181181
caller=caller,
182182
)
183183
response = await self.request_async(
184-
spec.method,
185-
url=spec.endpoint,
186-
json=spec.json,
187-
headers=_headers_with_trace(spec.headers, trace_context),
188-
scoped="tenant",
184+
spec.method, **_request_options(spec, trace_context)
189185
)
190-
return _to_run_result(_WireResponse.model_validate(response.json()))
186+
return _to_run_result(response.json())
187+
188+
def _prepare_run(
189+
self,
190+
name: str,
191+
input: Dict[str, Any],
192+
folder_key: Optional[str],
193+
folder_path: Optional[str],
194+
) -> _RunTarget:
195+
"""Do what run() and run_async() share before the first network call.
196+
197+
Applies a binding, validates the arguments and picks the folder, so only
198+
the folder lookup and the request itself differ between the two.
199+
"""
200+
name, folder_key, folder_path = self._apply_binding(
201+
name, folder_key, folder_path
202+
)
203+
_validate_run_arguments(name, input)
204+
selected_key, selected_path = self._select_folder(folder_key, folder_path)
205+
return _RunTarget(name=name, folder_key=selected_key, folder_path=selected_path)
191206

192207
@resource_override(resource_type="businessRule")
193208
def _overridden_resource(
@@ -240,25 +255,21 @@ def _select_folder(
240255
"folder_path, or set UIPATH_FOLDER_KEY or UIPATH_FOLDER_PATH"
241256
)
242257

243-
def _resolve_folder_key(
244-
self, folder_key: Optional[str], folder_path: Optional[str]
245-
) -> str:
258+
def _resolve_folder_key(self, target: _RunTarget) -> str:
246259
"""Return the key of the folder the rule runs in, looking up a path."""
247-
selected_key, selected_path = self._select_folder(folder_key, folder_path)
248-
if selected_path:
249-
selected_key = self._folders_service.retrieve_folder_key(selected_path)
250-
return _require_folder_key(selected_key, selected_path)
260+
folder_key = target.folder_key
261+
if target.folder_path:
262+
folder_key = self._folders_service.retrieve_folder_key(target.folder_path)
263+
return _require_folder_key(folder_key, target.folder_path)
251264

252-
async def _resolve_folder_key_async(
253-
self, folder_key: Optional[str], folder_path: Optional[str]
254-
) -> str:
265+
async def _resolve_folder_key_async(self, target: _RunTarget) -> str:
255266
"""Asynchronously return the key of the folder the rule runs in."""
256-
selected_key, selected_path = self._select_folder(folder_key, folder_path)
257-
if selected_path:
258-
selected_key = await self._folders_service.retrieve_folder_key_async(
259-
selected_path
267+
folder_key = target.folder_key
268+
if target.folder_path:
269+
folder_key = await self._folders_service.retrieve_folder_key_async(
270+
target.folder_path
260271
)
261-
return _require_folder_key(selected_key, selected_path)
272+
return _require_folder_key(folder_key, target.folder_path)
262273

263274
def _evaluate_spec(
264275
self,
@@ -288,6 +299,18 @@ def _evaluate_spec(
288299
)
289300

290301

302+
def _request_options(
303+
spec: RequestSpec, trace_context: Optional[TraceContext]
304+
) -> Dict[str, Any]:
305+
"""Return the request() arguments for a spec, besides its method."""
306+
return {
307+
"url": spec.endpoint,
308+
"json": spec.json,
309+
"headers": _headers_with_trace(spec.headers, trace_context),
310+
"scoped": "tenant",
311+
}
312+
313+
291314
def _require_folder_key(folder_key: Optional[str], folder_path: Optional[str]) -> str:
292315
# A folder path that matches no folder looks up to None.
293316
if folder_key and folder_key.strip():
@@ -390,7 +413,8 @@ def _overall_status(
390413
return BusinessRuleStatus.PARTIAL_SUCCESS
391414

392415

393-
def _to_run_result(response: _WireResponse) -> BusinessRuleRunResult:
416+
def _to_run_result(response_body: Any) -> BusinessRuleRunResult:
417+
response = _WireResponse.model_validate(response_body)
394418
# A successful response always carries the input's result; one without it is
395419
# not an answer to report as an evaluation.
396420
if response.result is None:

‎packages/uipath-platform/tests/services/test_business_rules_service.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,24 @@ def test_sends_no_explain_or_batch(
378378
assert "explain" not in body
379379
assert "inputs" not in body
380380

381+
async def test_run_async_names_itself_in_the_user_agent(
382+
self,
383+
httpx_mock: HTTPXMock,
384+
service: BusinessRulesService,
385+
evaluate_url: str,
386+
version: str,
387+
) -> None:
388+
httpx_mock.add_response(url=evaluate_url, json=_response())
389+
390+
await service.run_async(RULE, {}, folder_key=FOLDER_KEY)
391+
392+
request = httpx_mock.get_request()
393+
assert request is not None
394+
assert (
395+
request.headers[HEADER_USER_AGENT]
396+
== f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.BusinessRulesService.run_async/{version}"
397+
)
398+
381399
async def test_run_async_resolves_folder_path(
382400
self,
383401
httpx_mock: HTTPXMock,

0 commit comments

Comments
 (0)