|
6 | 6 |
|
7 | 7 | import unicodedata |
8 | 8 | from collections.abc import Mapping |
| 9 | +from dataclasses import dataclass |
9 | 10 | from typing import Any, Dict, List, Optional, Tuple |
10 | 11 |
|
11 | 12 | from ..common._base_service import _TRACE_PARENT_HEADER, BaseService |
|
34 | 35 | _FORBIDDEN_RULE_NAME_PARTS = ("/", "\\", "..", "%") |
35 | 36 |
|
36 | 37 |
|
| 38 | +@dataclass(frozen=True) |
| 39 | +class _RunTarget: |
| 40 | + """Where a run goes, settled before anything is sent. |
| 41 | +
|
| 42 | + A debug run carries ``debug_job_key`` and no folder. A deployed run carries |
| 43 | + exactly one of ``folder_key`` and ``folder_path``: a key ready to send, or a |
| 44 | + path still to look up. |
| 45 | + """ |
| 46 | + |
| 47 | + name: str |
| 48 | + folder_key: Optional[str] = None |
| 49 | + folder_path: Optional[str] = None |
| 50 | + debug_job_key: Optional[str] = None |
| 51 | + |
| 52 | + |
37 | 53 | class BusinessRulesService(FolderContext, BaseService): |
38 | 54 | """Service for running UiPath Business Rules. |
39 | 55 |
|
@@ -129,32 +145,19 @@ def run( |
129 | 145 | ) |
130 | 146 | ``` |
131 | 147 | """ |
132 | | - debug_job_key = _resolve_debug_job_key(folder_key, folder_path) |
133 | | - name, folder_key, folder_path = self._apply_binding( |
134 | | - name, folder_key, folder_path |
135 | | - ) |
136 | | - _validate_run_arguments(name, input) |
137 | | - if debug_job_key: |
138 | | - spec = self._debug_evaluate_spec( |
139 | | - name, input, job_key=debug_job_key, decision_names=decision_names |
140 | | - ) |
141 | | - else: |
142 | | - spec = self._evaluate_spec( |
143 | | - name, |
144 | | - input, |
145 | | - folder_key=self._resolve_folder_key(folder_key, folder_path), |
146 | | - version=version, |
147 | | - decision_names=decision_names, |
148 | | - caller=caller, |
149 | | - ) |
150 | | - response = self.request( |
151 | | - spec.method, |
152 | | - url=spec.endpoint, |
153 | | - json=spec.json, |
154 | | - headers=_headers_with_trace(spec.headers, trace_context), |
155 | | - scoped="tenant", |
| 148 | + target = self._prepare_run(name, input, folder_key, folder_path) |
| 149 | + spec = self._run_spec( |
| 150 | + target, |
| 151 | + input, |
| 152 | + folder_key=self._resolve_folder_key(target), |
| 153 | + version=version, |
| 154 | + decision_names=decision_names, |
| 155 | + caller=caller, |
156 | 156 | ) |
157 | | - return _to_run_result(_WireResponse.model_validate(response.json())) |
| 157 | + # Called here, not in a helper: BaseService names the user agent after |
| 158 | + # the method that calls request(), which must be the public run(). |
| 159 | + response = self.request(spec.method, **_request_options(spec, trace_context)) |
| 160 | + return _to_run_result(response.json()) |
158 | 161 |
|
159 | 162 | async def run_async( |
160 | 163 | self, |
@@ -193,34 +196,43 @@ async def run_async( |
193 | 196 | ValueError: If the request is invalid or a required folder is missing. |
194 | 197 | EnrichedException: If the service rejects the request. |
195 | 198 | """ |
| 199 | + target = self._prepare_run(name, input, folder_key, folder_path) |
| 200 | + spec = self._run_spec( |
| 201 | + target, |
| 202 | + input, |
| 203 | + folder_key=await self._resolve_folder_key_async(target), |
| 204 | + version=version, |
| 205 | + decision_names=decision_names, |
| 206 | + caller=caller, |
| 207 | + ) |
| 208 | + response = await self.request_async( |
| 209 | + spec.method, **_request_options(spec, trace_context) |
| 210 | + ) |
| 211 | + return _to_run_result(response.json()) |
| 212 | + |
| 213 | + def _prepare_run( |
| 214 | + self, |
| 215 | + name: str, |
| 216 | + input: Dict[str, Any], |
| 217 | + folder_key: Optional[str], |
| 218 | + folder_path: Optional[str], |
| 219 | + ) -> _RunTarget: |
| 220 | + """Do what run() and run_async() share before the first network call. |
| 221 | +
|
| 222 | + Decides between a debug and a deployed run, applies a binding, validates |
| 223 | + the arguments and, for a deployed run, picks the folder, so only the |
| 224 | + folder lookup and the request itself differ between the two. |
| 225 | + """ |
| 226 | + # Decided on the caller's own folder arguments, before a binding swaps them. |
196 | 227 | debug_job_key = _resolve_debug_job_key(folder_key, folder_path) |
197 | 228 | name, folder_key, folder_path = self._apply_binding( |
198 | 229 | name, folder_key, folder_path |
199 | 230 | ) |
200 | 231 | _validate_run_arguments(name, input) |
201 | 232 | if debug_job_key: |
202 | | - spec = self._debug_evaluate_spec( |
203 | | - name, input, job_key=debug_job_key, decision_names=decision_names |
204 | | - ) |
205 | | - else: |
206 | | - spec = self._evaluate_spec( |
207 | | - name, |
208 | | - input, |
209 | | - folder_key=await self._resolve_folder_key_async( |
210 | | - folder_key, folder_path |
211 | | - ), |
212 | | - version=version, |
213 | | - decision_names=decision_names, |
214 | | - caller=caller, |
215 | | - ) |
216 | | - response = await self.request_async( |
217 | | - spec.method, |
218 | | - url=spec.endpoint, |
219 | | - json=spec.json, |
220 | | - headers=_headers_with_trace(spec.headers, trace_context), |
221 | | - scoped="tenant", |
222 | | - ) |
223 | | - return _to_run_result(_WireResponse.model_validate(response.json())) |
| 233 | + return _RunTarget(name=name, debug_job_key=debug_job_key) |
| 234 | + selected_key, selected_path = self._select_folder(folder_key, folder_path) |
| 235 | + return _RunTarget(name=name, folder_key=selected_key, folder_path=selected_path) |
224 | 236 |
|
225 | 237 | @resource_override(resource_type="businessRule") |
226 | 238 | def _overridden_resource( |
@@ -273,25 +285,50 @@ def _select_folder( |
273 | 285 | "folder_path, or set UIPATH_FOLDER_KEY or UIPATH_FOLDER_PATH" |
274 | 286 | ) |
275 | 287 |
|
276 | | - def _resolve_folder_key( |
277 | | - self, folder_key: Optional[str], folder_path: Optional[str] |
278 | | - ) -> str: |
279 | | - """Return the key of the folder the rule runs in, looking up a path.""" |
280 | | - selected_key, selected_path = self._select_folder(folder_key, folder_path) |
281 | | - if selected_path: |
282 | | - selected_key = self._folders_service.retrieve_folder_key(selected_path) |
283 | | - return _require_folder_key(selected_key, selected_path) |
| 288 | + def _resolve_folder_key(self, target: _RunTarget) -> Optional[str]: |
| 289 | + """Return the key of the folder the rule runs in, looking up a path. |
284 | 290 |
|
285 | | - async def _resolve_folder_key_async( |
286 | | - self, folder_key: Optional[str], folder_path: Optional[str] |
287 | | - ) -> str: |
| 291 | + None for a debug run, which names no folder, and for a path that |
| 292 | + matches no folder, which _run_spec refuses. |
| 293 | + """ |
| 294 | + if target.folder_path: |
| 295 | + return self._folders_service.retrieve_folder_key(target.folder_path) |
| 296 | + return target.folder_key |
| 297 | + |
| 298 | + async def _resolve_folder_key_async(self, target: _RunTarget) -> Optional[str]: |
288 | 299 | """Asynchronously return the key of the folder the rule runs in.""" |
289 | | - selected_key, selected_path = self._select_folder(folder_key, folder_path) |
290 | | - if selected_path: |
291 | | - selected_key = await self._folders_service.retrieve_folder_key_async( |
292 | | - selected_path |
| 300 | + if target.folder_path: |
| 301 | + return await self._folders_service.retrieve_folder_key_async( |
| 302 | + target.folder_path |
293 | 303 | ) |
294 | | - return _require_folder_key(selected_key, selected_path) |
| 304 | + return target.folder_key |
| 305 | + |
| 306 | + def _run_spec( |
| 307 | + self, |
| 308 | + target: _RunTarget, |
| 309 | + input: Dict[str, Any], |
| 310 | + *, |
| 311 | + folder_key: Optional[str], |
| 312 | + version: Optional[str] = None, |
| 313 | + decision_names: Optional[List[str]] = None, |
| 314 | + caller: Optional[BusinessRuleCaller] = None, |
| 315 | + ) -> RequestSpec: |
| 316 | + """Build the request for the endpoint the target names.""" |
| 317 | + if target.debug_job_key: |
| 318 | + return self._debug_evaluate_spec( |
| 319 | + target.name, |
| 320 | + input, |
| 321 | + job_key=target.debug_job_key, |
| 322 | + decision_names=decision_names, |
| 323 | + ) |
| 324 | + return self._evaluate_spec( |
| 325 | + target.name, |
| 326 | + input, |
| 327 | + folder_key=_require_folder_key(folder_key, target.folder_path), |
| 328 | + version=version, |
| 329 | + decision_names=decision_names, |
| 330 | + caller=caller, |
| 331 | + ) |
295 | 332 |
|
296 | 333 | def _evaluate_spec( |
297 | 334 | self, |
@@ -360,6 +397,18 @@ def _resolve_debug_job_key( |
360 | 397 | return job_key if job_key and job_key.strip() else None |
361 | 398 |
|
362 | 399 |
|
| 400 | +def _request_options( |
| 401 | + spec: RequestSpec, trace_context: Optional[TraceContext] |
| 402 | +) -> Dict[str, Any]: |
| 403 | + """Return the request() arguments for a spec, besides its method.""" |
| 404 | + return { |
| 405 | + "url": spec.endpoint, |
| 406 | + "json": spec.json, |
| 407 | + "headers": _headers_with_trace(spec.headers, trace_context), |
| 408 | + "scoped": "tenant", |
| 409 | + } |
| 410 | + |
| 411 | + |
363 | 412 | def _require_folder_key(folder_key: Optional[str], folder_path: Optional[str]) -> str: |
364 | 413 | # A folder path that matches no folder looks up to None. |
365 | 414 | if folder_key and folder_key.strip(): |
@@ -462,7 +511,8 @@ def _overall_status( |
462 | 511 | return BusinessRuleStatus.PARTIAL_SUCCESS |
463 | 512 |
|
464 | 513 |
|
465 | | -def _to_run_result(response: _WireResponse) -> BusinessRuleRunResult: |
| 514 | +def _to_run_result(response_body: Any) -> BusinessRuleRunResult: |
| 515 | + response = _WireResponse.model_validate(response_body) |
466 | 516 | # A successful response always carries the input's result; one without it is |
467 | 517 | # not an answer to report as an evaluation. |
468 | 518 | if response.result is None: |
|
0 commit comments