|
5 | 5 |
|
6 | 6 | import unicodedata |
7 | 7 | from collections.abc import Mapping |
| 8 | +from dataclasses import dataclass |
8 | 9 | from typing import Any, Dict, List, Optional, Tuple |
9 | 10 |
|
10 | 11 | from ..common._base_service import _TRACE_PARENT_HEADER, BaseService |
|
32 | 33 | _FORBIDDEN_RULE_NAME_PARTS = ("/", "\\", "..", "%") |
33 | 34 |
|
34 | 35 |
|
| 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 | + |
35 | 49 | class BusinessRulesService(FolderContext, BaseService): |
36 | 50 | """Service for running UiPath Business Rules. |
37 | 51 |
|
@@ -108,27 +122,19 @@ def run( |
108 | 122 | print(decision.decision_name, decision.outputs) |
109 | 123 | ``` |
110 | 124 | """ |
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) |
116 | 126 | spec = self._evaluate_spec( |
117 | | - name, |
| 127 | + target.name, |
118 | 128 | input, |
119 | | - folder_key=resolved_folder_key, |
| 129 | + folder_key=self._resolve_folder_key(target), |
120 | 130 | version=version, |
121 | 131 | decision_names=decision_names, |
122 | 132 | caller=caller, |
123 | 133 | ) |
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()) |
132 | 138 |
|
133 | 139 | async def run_async( |
134 | 140 | self, |
@@ -165,29 +171,38 @@ async def run_async( |
165 | 171 | ValueError: If the request is invalid or a required folder is missing. |
166 | 172 | EnrichedException: If the service rejects the request. |
167 | 173 | """ |
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) |
175 | 175 | spec = self._evaluate_spec( |
176 | | - name, |
| 176 | + target.name, |
177 | 177 | input, |
178 | | - folder_key=resolved_folder_key, |
| 178 | + folder_key=await self._resolve_folder_key_async(target), |
179 | 179 | version=version, |
180 | 180 | decision_names=decision_names, |
181 | 181 | caller=caller, |
182 | 182 | ) |
183 | 183 | 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) |
189 | 185 | ) |
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) |
191 | 206 |
|
192 | 207 | @resource_override(resource_type="businessRule") |
193 | 208 | def _overridden_resource( |
@@ -240,25 +255,21 @@ def _select_folder( |
240 | 255 | "folder_path, or set UIPATH_FOLDER_KEY or UIPATH_FOLDER_PATH" |
241 | 256 | ) |
242 | 257 |
|
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: |
246 | 259 | """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) |
251 | 264 |
|
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: |
255 | 266 | """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 |
260 | 271 | ) |
261 | | - return _require_folder_key(selected_key, selected_path) |
| 272 | + return _require_folder_key(folder_key, target.folder_path) |
262 | 273 |
|
263 | 274 | def _evaluate_spec( |
264 | 275 | self, |
@@ -288,6 +299,18 @@ def _evaluate_spec( |
288 | 299 | ) |
289 | 300 |
|
290 | 301 |
|
| 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 | + |
291 | 314 | def _require_folder_key(folder_key: Optional[str], folder_path: Optional[str]) -> str: |
292 | 315 | # A folder path that matches no folder looks up to None. |
293 | 316 | if folder_key and folder_key.strip(): |
@@ -390,7 +413,8 @@ def _overall_status( |
390 | 413 | return BusinessRuleStatus.PARTIAL_SUCCESS |
391 | 414 |
|
392 | 415 |
|
393 | | -def _to_run_result(response: _WireResponse) -> BusinessRuleRunResult: |
| 416 | +def _to_run_result(response_body: Any) -> BusinessRuleRunResult: |
| 417 | + response = _WireResponse.model_validate(response_body) |
394 | 418 | # A successful response always carries the input's result; one without it is |
395 | 419 | # not an answer to report as an evaluation. |
396 | 420 | if response.result is None: |
|
0 commit comments