uipath 2.0.0.dev2__py3-none-any.whl → 2.0.1.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of uipath might be problematic. Click here for more details.

Files changed (76) hide show
  1. uipath/__init__.py +24 -0
  2. uipath/_cli/README.md +11 -0
  3. uipath/_cli/__init__.py +54 -0
  4. uipath/_cli/_auth/_auth_server.py +165 -0
  5. uipath/_cli/_auth/_models.py +51 -0
  6. uipath/_cli/_auth/_oidc_utils.py +69 -0
  7. uipath/_cli/_auth/_portal_service.py +163 -0
  8. uipath/_cli/_auth/_utils.py +51 -0
  9. uipath/_cli/_auth/auth_config.json +6 -0
  10. uipath/_cli/_auth/index.html +167 -0
  11. uipath/_cli/_auth/localhost.crt +25 -0
  12. uipath/_cli/_auth/localhost.key +27 -0
  13. uipath/_cli/_runtime/_contracts.py +429 -0
  14. uipath/_cli/_runtime/_logging.py +193 -0
  15. uipath/_cli/_runtime/_runtime.py +264 -0
  16. uipath/_cli/_templates/.psmdcp.template +9 -0
  17. uipath/_cli/_templates/.rels.template +5 -0
  18. uipath/_cli/_templates/[Content_Types].xml.template +9 -0
  19. uipath/_cli/_templates/main.py.template +25 -0
  20. uipath/_cli/_templates/package.nuspec.template +10 -0
  21. uipath/_cli/_utils/_common.py +24 -0
  22. uipath/_cli/_utils/_input_args.py +126 -0
  23. uipath/_cli/_utils/_parse_ast.py +542 -0
  24. uipath/_cli/cli_auth.py +97 -0
  25. uipath/_cli/cli_deploy.py +13 -0
  26. uipath/_cli/cli_init.py +113 -0
  27. uipath/_cli/cli_new.py +76 -0
  28. uipath/_cli/cli_pack.py +337 -0
  29. uipath/_cli/cli_publish.py +113 -0
  30. uipath/_cli/cli_run.py +133 -0
  31. uipath/_cli/middlewares.py +113 -0
  32. uipath/_config.py +6 -0
  33. uipath/_execution_context.py +83 -0
  34. uipath/_folder_context.py +62 -0
  35. uipath/_models/__init__.py +37 -0
  36. uipath/_models/action_schema.py +26 -0
  37. uipath/_models/actions.py +64 -0
  38. uipath/_models/assets.py +48 -0
  39. uipath/_models/connections.py +51 -0
  40. uipath/_models/context_grounding.py +18 -0
  41. uipath/_models/context_grounding_index.py +60 -0
  42. uipath/_models/exceptions.py +6 -0
  43. uipath/_models/interrupt_models.py +28 -0
  44. uipath/_models/job.py +66 -0
  45. uipath/_models/llm_gateway.py +101 -0
  46. uipath/_models/processes.py +48 -0
  47. uipath/_models/queues.py +167 -0
  48. uipath/_services/__init__.py +26 -0
  49. uipath/_services/_base_service.py +250 -0
  50. uipath/_services/actions_service.py +271 -0
  51. uipath/_services/api_client.py +89 -0
  52. uipath/_services/assets_service.py +257 -0
  53. uipath/_services/buckets_service.py +268 -0
  54. uipath/_services/connections_service.py +185 -0
  55. uipath/_services/connections_service.pyi +50 -0
  56. uipath/_services/context_grounding_service.py +402 -0
  57. uipath/_services/folder_service.py +49 -0
  58. uipath/_services/jobs_service.py +265 -0
  59. uipath/_services/llm_gateway_service.py +311 -0
  60. uipath/_services/processes_service.py +168 -0
  61. uipath/_services/queues_service.py +314 -0
  62. uipath/_uipath.py +98 -0
  63. uipath/_utils/__init__.py +17 -0
  64. uipath/_utils/_endpoint.py +79 -0
  65. uipath/_utils/_infer_bindings.py +30 -0
  66. uipath/_utils/_logs.py +15 -0
  67. uipath/_utils/_request_override.py +18 -0
  68. uipath/_utils/_request_spec.py +23 -0
  69. uipath/_utils/_user_agent.py +16 -0
  70. uipath/_utils/constants.py +25 -0
  71. uipath/py.typed +0 -0
  72. {uipath-2.0.0.dev2.dist-info → uipath-2.0.1.dev1.dist-info}/METADATA +2 -3
  73. uipath-2.0.1.dev1.dist-info/RECORD +75 -0
  74. uipath-2.0.0.dev2.dist-info/RECORD +0 -4
  75. {uipath-2.0.0.dev2.dist-info → uipath-2.0.1.dev1.dist-info}/WHEEL +0 -0
  76. {uipath-2.0.0.dev2.dist-info → uipath-2.0.1.dev1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,167 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>UiPath CLI Authentication</title>
8
+ <style>
9
+ button {
10
+ opacity: 0;
11
+ }
12
+ </style>
13
+ </head>
14
+
15
+ <body>
16
+ <h1>UiPath CLI Authentication</h1>
17
+ <button type="button" onclick="window.open('', '_self', ''); window.close();">Discard</button>
18
+ <pre id="log"></pre>
19
+ <script>
20
+ const baseUrl = '__PY_REPLACE_REDIRECT_URI__'.replace('/oidc/login', '');
21
+ const logs = [];
22
+ // Parse URL query parameters
23
+ function getQueryParams() {
24
+ const params = {};
25
+ const queryString = window.location.search;
26
+ const urlParams = new URLSearchParams(queryString);
27
+
28
+ for (const [key, value] of urlParams.entries()) {
29
+ params[key] = value;
30
+ }
31
+ return params;
32
+ }
33
+
34
+ // Exchange authorization code for tokens
35
+ async function exchangeCodeForToken(code, codeVerifier) {
36
+ try {
37
+ // Prepare form data for token request
38
+ const formData = new URLSearchParams();
39
+ formData.append('grant_type', 'authorization_code');
40
+ formData.append('code', code);
41
+ formData.append('redirect_uri', '__PY_REPLACE_REDIRECT_URI__');
42
+ formData.append('client_id', '__PY_REPLACE_CLIENT_ID__');
43
+ formData.append('code_verifier', codeVerifier);
44
+
45
+ // Make token request
46
+ const response = await fetch('https://__PY_REPLACE_DOMAIN__.uipath.com/identity_/connect/token', {
47
+ method: 'POST',
48
+ headers: {
49
+ 'Content-Type': 'application/x-www-form-urlencoded'
50
+ },
51
+ body: formData
52
+ });
53
+
54
+ if (!response.ok) {
55
+ const errorText = await response.text();
56
+ throw new Error(`Token request failed: ${response.status} ${response.statusText} - ${errorText}`);
57
+ }
58
+
59
+ const tokenData = await response.json();
60
+
61
+ return tokenData;
62
+ } catch (error) {
63
+ console.error('Error exchanging code for token:', error);
64
+ logs.push({
65
+ timestamp: new Date().toISOString(),
66
+ message: 'Error exchanging code for token:',
67
+ error: error.message
68
+ });
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ // Parse JWT token
74
+ function parseJwt(token) {
75
+ try {
76
+ const base64Url = token.split('.')[1];
77
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
78
+ const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
79
+ return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
80
+ }).join(''));
81
+
82
+ return JSON.parse(jsonPayload);
83
+ } catch (e) {
84
+ console.error('Error parsing JWT:', e);
85
+ logs.push({
86
+ timestamp: new Date().toISOString(),
87
+ message: 'Error parsing JWT:',
88
+ error: e.message
89
+ });
90
+ return null;
91
+ }
92
+ }
93
+
94
+ async function delay(ms) {
95
+ return new Promise(resolve => setTimeout(resolve, ms));
96
+ }
97
+
98
+ async function sendLogs(logs) {
99
+ await fetch(`${baseUrl}/log`, {
100
+ method: 'POST',
101
+ body: JSON.stringify(logs)
102
+ });
103
+ }
104
+
105
+ // Main function to handle the authentication flow
106
+ async function handleAuthentication() {
107
+ const params = getQueryParams();
108
+
109
+ logs.push({
110
+ timestamp: new Date().toISOString(),
111
+ message: 'Params:',
112
+ params: params
113
+ });
114
+
115
+ try {
116
+ // Check if we have an authorization code
117
+ if (params.code && params.state) {
118
+ // Get code verifier from session storage
119
+ const codeVerifier = "__PY_REPLACE_CODE_VERIFIER__";
120
+ const state = "__PY_REPLACE_EXPECTED_STATE__";
121
+
122
+ if (!codeVerifier) {
123
+ throw new Error('Code verifier not found in session storage');
124
+ }
125
+
126
+ if (!params.state || params.state != state) {
127
+ throw new Error('Invalid state parameter. Possible CSRF attack.');
128
+ }
129
+
130
+ // Exchange code for token
131
+ const tokenData = await exchangeCodeForToken(params.code, codeVerifier);
132
+
133
+ await sendLogs(logs);
134
+ const setTokenResult = await fetch(`${baseUrl}/set_token`, {
135
+ method: 'POST',
136
+ body: JSON.stringify(tokenData)
137
+ });
138
+ // Show success message
139
+ document.querySelector('h1').textContent = 'If this windows does not close automatically, you may close it now';
140
+ setTimeout(() => {
141
+ document.querySelector('button').click();
142
+ }, 500);
143
+
144
+ sessionStorage.removeItem('oidc_state');
145
+ sessionStorage.removeItem('oidc_code_verifier');
146
+
147
+ // Remove code and state from URL to prevent refresh issues
148
+ window.history.replaceState({}, document.title, window.location.pathname);
149
+ }
150
+ } catch (error) {
151
+ console.error('Error during authentication:', error);
152
+ logs.push({
153
+ timestamp: new Date().toISOString(),
154
+ message: 'Error during authentication:',
155
+ error: error.message
156
+ });
157
+ await sendLogs(logs);
158
+ document.querySelector('h1').textContent = 'Authentication failed. Please try again.';
159
+ }
160
+ }
161
+
162
+ // Start authentication process when page loads
163
+ handleAuthentication();
164
+ </script>
165
+ </body>
166
+
167
+ </html>
@@ -0,0 +1,25 @@
1
+ Bag Attributes
2
+ localKeyID: 01 00 00 00
3
+ subject=CN = localhost
4
+
5
+ issuer=CN = localhost
6
+
7
+ -----BEGIN CERTIFICATE-----
8
+ MIIDGDCCAgCgAwIBAgIQFUZ3VJelF55GG4DYbK0CcTANBgkqhkiG9w0BAQsFADAU
9
+ MRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMjAwOTE1MDczOTQ5WhcNMzAwOTE1MDc0
10
+ OTQ5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IB
11
+ DwAwggEKAoIBAQDDFJklpJ0xEEzVo8qHb2l3Pagh5+Sf4mHSE6BsbDx2TZhzTtEP
12
+ 4LMl9XlmTIZsZmWq/G7TYp4xxmqktFfeqlTQKf0mpZnZMpa9GKUFoMYBt1b1UjSw
13
+ Y9i2Ksli4a91zxkABRkK6f54lj2F5lK3BcE8th0VwsUxvI+/9ebukVQrVgPxp2Zl
14
+ KB6uC1ISQdwzExw25wkbQmfkS7HnM/GfgEVhiVM8tlKP1I9IjgNqWGJYrKTvtjW5
15
+ 0DWUBBtyB+IGjShapWTS9cf6U5DELhO2SKWG4BKdDdsMcXEArMFHL1O/WCpgWTdS
16
+ Pc/SBafr/heQzCjStV1t4ilQA4J3+rWtpSx9AgMBAAGjZjBkMA4GA1UdDwEB/wQE
17
+ AwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwFAYDVR0RBA0wC4IJ
18
+ bG9jYWxob3N0MB0GA1UdDgQWBBQgVvcFuUmsa5WAy1YlQPW1CaDydjANBgkqhkiG
19
+ 9w0BAQsFAAOCAQEADLjzbEa6jBOAwYwa76NJwEVkD19guO+mL0uLDlrq+UffkBaB
20
+ 7uNGNqf+4Z9LEhX4aBTcXLmbux7LcKBmMhsdMhv90UT3zmyHHMzVXw0aHs5RLiaQ
21
+ oY6RYlkjnTb4ruZRhWz4FNMLWIXqp216JGPcYSlOChn6vB5cofTTB2DtFAKYT4OR
22
+ meC+Wt7o80E0NZO8FFgKs7Ay2ySatm3olaRmhPj0T4aJbdez1xxc6CbdpEoRrwii
23
+ favXxJCe554X6C0uWpZqNA5Id3NTs37af5n6nEvtHhqdaN9PazCNKxX8UUebI79E
24
+ MZPTClhX0akFWjzgJg2QZadOv7lQtH141t3xlw==
25
+ -----END CERTIFICATE-----
@@ -0,0 +1,27 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIIEpQIBAAKCAQEAwxSZJaSdMRBM1aPKh29pdz2oIefkn+Jh0hOgbGw8dk2Yc07R
3
+ D+CzJfV5ZkyGbGZlqvxu02KeMcZqpLRX3qpU0Cn9JqWZ2TKWvRilBaDGAbdW9VI0
4
+ sGPYtirJYuGvdc8ZAAUZCun+eJY9heZStwXBPLYdFcLFMbyPv/Xm7pFUK1YD8adm
5
+ ZSgergtSEkHcMxMcNucJG0Jn5Eux5zPxn4BFYYlTPLZSj9SPSI4DalhiWKyk77Y1
6
+ udA1lAQbcgfiBo0oWqVk0vXH+lOQxC4TtkilhuASnQ3bDHFxAKzBRy9Tv1gqYFk3
7
+ Uj3P0gWn6/4XkMwo0rVdbeIpUAOCd/q1raUsfQIDAQABAoIBAQCvxV2AJ2RjEtbI
8
+ ID6Z7W7Pu8REWJcM4INXL8v6JUQNLDlSnA+PCN0iNJ3f6M+t2E1+1NheqprzKl5o
9
+ bP5HaJ3mug6YxdlrdEORuiTmf0gpv7IgdKN4014uNTsjV0orEiDqfa1kwhVBPc3N
10
+ WgvpUMweEdD/DHF1MPss8TnUdympqNHG+63jh3q2P4WCxqnIyjcyr6KoiFXPwPz4
11
+ iDMa2mHAQTdwxraEZve4o9TlMGIHZEiG0M8HKGM+wHx7Wt4Y6pf2pstpUg141bg6
12
+ KN6za/4iBRs5Fi36F1xONHO3FqBDbhgzJ4zwXFVHxzMyfxTdNjhq6QMqHbP+iZ+b
13
+ 0p0djq9ZAoGBAOsIUjKjTv0TTev6ZwzkGJN7d2qv7a/dxOilWqjjyRcn31z5srwR
14
+ ofm7DVIHmjJFPlTtMjA7jARBp8O2bXSs47Qpi0pLTrNoJUOHfHqJbhyzFqdPxr39
15
+ wxICnyeelsGJsxi9XbysezwRlM84tbh100hxQ2rL3/gojbmAZtPpmomDAoGBANR7
16
+ 2e2fv9aJEW97b58GMo4WMkkkm+ukLBaeWbtzHColtWiTQookI1upJYX8HuCTc4Po
17
+ SjuC5iY+kR2LhNBM3qlhcjAr14CYKsbeeVtjGkqdlNXLJth0SUvDSgAGoJpZ3ESE
18
+ 40DDSWO1FIjsXDQ4HjsqcPERNtJ6hroOWIKWcZH/AoGAGMqy6PaDAX0EziyGrDxv
19
+ PzdZ3F4cY12wRJ4UlxYYAyJIlGUebmopvM73U7zLjhvxvEGc9quBVH2cda+LkjAe
20
+ gMWVFMwmzBPjQmFLRx2yCML3U8S9gRMAfajbIvG+k0G0K5rmBV3oTejI2qbvvDpp
21
+ hA22f9BrmqCkennJ3vaYP4MCgYEAhQBZMvIxpFn7vepnvgSlFGc0ZrAxoNgMeMP5
22
+ qumskX01BhZXKhzgvPktcaaUtUHWppR6ErIm2X2EXBf+tFvJbACA90IaDHYzIHQV
23
+ RmzMAjSM+x3EXOy9DF1bLpT3ZmYlCAaeMOWMRIsOJX69YDpYQfSY6Ww7ApiLSF6+
24
+ bE87G4UCgYEAxFuPU5hSLi17i8+PlUnCfpVadtGveUwgunvgfXzRmCUz1RPRhv9e
25
+ XSgGuMiQv9Z0tfpFMRrYlkqxjr4DPX7wLHek9lqrDnEC/YSpKyxI1ylFGEcZi4mq
26
+ CDLxmUyI3ZuifuLtWj66C1uP+FVz9x9PieGgdAE+wHfdlyagTACd5rE=
27
+ -----END RSA PRIVATE KEY-----
@@ -0,0 +1,429 @@
1
+ """Core runtime contracts that define the interfaces between components."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import sys
7
+ import traceback
8
+ from abc import ABC, abstractmethod
9
+ from enum import Enum
10
+ from functools import cached_property
11
+ from typing import Any, Dict, Optional, Union
12
+
13
+ from pydantic import BaseModel, Field
14
+
15
+ from ._logging import LogsInterceptor
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class UiPathResumeTriggerType(str, Enum):
21
+ """Constants representing different types of resume job triggers in the system."""
22
+
23
+ NONE = "None"
24
+ QUEUE_ITEM = "QueueItem"
25
+ JOB = "Job"
26
+ ACTION = "Task"
27
+ TIMER = "Timer"
28
+ INBOX = "Inbox"
29
+ API = "Api"
30
+
31
+
32
+ class UiPathApiTrigger(BaseModel):
33
+ """API resume trigger request."""
34
+
35
+ inbox_id: Optional[str] = Field(default=None, alias="inboxId")
36
+ request: Any = None
37
+
38
+ model_config = {"populate_by_name": True}
39
+
40
+
41
+ class UiPathResumeTrigger(BaseModel):
42
+ """Information needed to resume execution."""
43
+
44
+ trigger_type: UiPathResumeTriggerType = Field(
45
+ default=UiPathResumeTriggerType.API, alias="triggerType"
46
+ )
47
+ item_key: Optional[str] = Field(default=None, alias="itemKey")
48
+ api_resume: Optional[UiPathApiTrigger] = Field(default=None, alias="apiResume")
49
+
50
+ model_config = {"populate_by_name": True}
51
+
52
+
53
+ class UiPathErrorCategory(str, Enum):
54
+ """Categories of runtime errors."""
55
+
56
+ DEPLOYMENT = "Deployment" # Configuration, licensing, or permission issues
57
+ SYSTEM = "System" # Unexpected internal errors or infrastructure issues
58
+ UNKNOWN = "Unknown" # Default category when the error type is not specified
59
+ USER = "User" # Business logic or domain-level errors
60
+
61
+
62
+ class UiPathErrorContract(BaseModel):
63
+ """Standard error contract used across the runtime."""
64
+
65
+ code: str # Human-readable code uniquely identifying this error type across the platform.
66
+ # Format: <Component>.<PascalCaseErrorCode> (e.g. LangGraph.InvaliGraphReference)
67
+ # Only use alphanumeric characters [A-Za-z0-9] and periods. No whitespace allowed.
68
+
69
+ title: str # Short, human-readable summary of the problem that should remain consistent
70
+ # across occurrences.
71
+
72
+ detail: (
73
+ str # Human-readable explanation specific to this occurrence of the problem.
74
+ )
75
+ # May include context, recommended actions, or technical details like call stacks
76
+ # for technical users.
77
+
78
+ category: UiPathErrorCategory = (
79
+ UiPathErrorCategory.UNKNOWN
80
+ ) # Classification of the error:
81
+ # - User: Business logic or domain-level errors
82
+ # - Deployment: Configuration, licensing, or permission issues
83
+ # - System: Unexpected internal errors or infrastructure issues
84
+
85
+ status: Optional[int] = (
86
+ None # HTTP status code, if relevant (e.g., when forwarded from a web API)
87
+ )
88
+
89
+
90
+ class UiPathRuntimeStatus(str, Enum):
91
+ """Standard status values for runtime execution."""
92
+
93
+ SUCCESSFUL = "successful"
94
+ FAULTED = "faulted"
95
+ SUSPENDED = "suspended"
96
+
97
+
98
+ class UiPathRuntimeResult(BaseModel):
99
+ """Result of an execution with status and optional error information."""
100
+
101
+ output: Optional[Dict[str, Any]] = None
102
+ status: UiPathRuntimeStatus = UiPathRuntimeStatus.SUCCESSFUL
103
+ resume: Optional[UiPathResumeTrigger] = None
104
+ error: Optional[UiPathErrorContract] = None
105
+
106
+ def to_dict(self) -> Dict[str, Any]:
107
+ """Convert to dictionary format for output."""
108
+ result = {
109
+ "output": self.output or {},
110
+ "status": self.status,
111
+ }
112
+
113
+ if self.resume:
114
+ result["resume"] = self.resume.model_dump(by_alias=True)
115
+
116
+ if self.error:
117
+ result["error"] = self.error.model_dump()
118
+
119
+ return result
120
+
121
+
122
+ class UiPathTraceContext(BaseModel):
123
+ """Trace context information for tracing and debugging."""
124
+
125
+ trace_id: Optional[str] = None
126
+ parent_span_id: Optional[str] = None
127
+ root_span_id: Optional[str] = None
128
+ org_id: Optional[str] = None
129
+ tenant_id: Optional[str] = None
130
+ job_id: Optional[str] = None
131
+ folder_key: Optional[str] = None
132
+ process_key: Optional[str] = None
133
+ enabled: Union[bool, str] = False
134
+ reference_id: Optional[str] = None
135
+
136
+
137
+ class UiPathRuntimeContext(BaseModel):
138
+ """Context information passed throughout the runtime execution."""
139
+
140
+ entrypoint: Optional[str] = None
141
+ input: Optional[str] = None
142
+ input_json: Optional[Any] = None
143
+ job_id: Optional[str] = None
144
+ trace_id: Optional[str] = None
145
+ trace_context: Optional[UiPathTraceContext] = None
146
+ tracing_enabled: Union[bool, str] = False
147
+ resume: bool = False
148
+ config_path: str = "uipath.json"
149
+ runtime_dir: Optional[str] = "__uipath"
150
+ logs_file: Optional[str] = "execution.log"
151
+ logs_min_level: Optional[str] = "INFO"
152
+ output_file: str = "output.json"
153
+ state_file: str = "state.db"
154
+ result: Optional[UiPathRuntimeResult] = None
155
+
156
+ model_config = {"arbitrary_types_allowed": True}
157
+
158
+ @classmethod
159
+ def from_config(cls, config_path=None):
160
+ """Load configuration from uipath.json file.
161
+
162
+ Args:
163
+ config_path: Path to the configuration file. If None, uses the default "uipath.json"
164
+
165
+ Returns:
166
+ An instance of the class with fields populated from the config file
167
+ """
168
+ import json
169
+ import os
170
+
171
+ path = config_path or "uipath.json"
172
+
173
+ config = {}
174
+
175
+ if os.path.exists(path):
176
+ with open(path, "r") as f:
177
+ config = json.load(f)
178
+
179
+ instance = cls()
180
+
181
+ if "runtime" in config:
182
+ runtime_config = config["runtime"]
183
+
184
+ mapping = {
185
+ "dir": "runtime_dir",
186
+ "outputFile": "output_file",
187
+ "stateFile": "state_file",
188
+ "logsFile": "logs_file",
189
+ }
190
+
191
+ for config_key, attr_name in mapping.items():
192
+ if config_key in runtime_config and hasattr(instance, attr_name):
193
+ setattr(instance, attr_name, runtime_config[config_key])
194
+
195
+ return instance
196
+
197
+
198
+ class UiPathRuntimeError(Exception):
199
+ """Base exception class for UiPath runtime errors with structured error information."""
200
+
201
+ def __init__(
202
+ self,
203
+ code: str,
204
+ title: str,
205
+ detail: str,
206
+ category: UiPathErrorCategory = UiPathErrorCategory.UNKNOWN,
207
+ status: Optional[int] = None,
208
+ prefix: str = "Python",
209
+ include_traceback: bool = True,
210
+ ):
211
+ # Get the current traceback as a string
212
+ if include_traceback:
213
+ tb = traceback.format_exc()
214
+ if (
215
+ tb and tb.strip() != "NoneType: None"
216
+ ): # Ensure there's an actual traceback
217
+ detail = f"{detail}\n\nTraceback:\n{tb}"
218
+
219
+ if status is None:
220
+ status = self._extract_http_status()
221
+
222
+ self.error_info = UiPathErrorContract(
223
+ code=f"{prefix}.{code}",
224
+ title=title,
225
+ detail=detail,
226
+ category=category,
227
+ status=status,
228
+ )
229
+ super().__init__(detail)
230
+
231
+ def _extract_http_status(self) -> Optional[int]:
232
+ """Extract HTTP status code from the exception chain if present."""
233
+ exc_info = sys.exc_info()
234
+ if not exc_info or len(exc_info) < 2 or exc_info[1] is None:
235
+ return None
236
+
237
+ exc: Optional[BaseException] = exc_info[1] # Current exception being handled
238
+ while exc is not None:
239
+ if hasattr(exc, "status_code"):
240
+ return exc.status_code
241
+
242
+ if hasattr(exc, "response") and hasattr(exc.response, "status_code"):
243
+ return exc.response.status_code
244
+
245
+ # Move to the next exception in the chain
246
+ next_exc = getattr(exc, "__cause__", None) or getattr(
247
+ exc, "__context__", None
248
+ )
249
+
250
+ # Ensure next_exc is a BaseException or None
251
+ exc = (
252
+ next_exc
253
+ if isinstance(next_exc, BaseException) or next_exc is None
254
+ else None
255
+ )
256
+
257
+ return None
258
+
259
+ @property
260
+ def as_dict(self) -> Dict[str, Any]:
261
+ """Get the error information as a dictionary."""
262
+ return self.error_info.model_dump()
263
+
264
+
265
+ class UiPathBaseRuntime(ABC):
266
+ """Base runtime class implementing the async context manager protocol.
267
+
268
+ This allows using the class with 'async with' statements.
269
+ """
270
+
271
+ def __init__(self, context: UiPathRuntimeContext):
272
+ self.context = context
273
+
274
+ @classmethod
275
+ def from_context(cls, context: UiPathRuntimeContext):
276
+ """Factory method to create a runtime instance from a context.
277
+
278
+ Args:
279
+ context: The runtime context with configuration
280
+
281
+ Returns:
282
+ An initialized Runtime instance
283
+ """
284
+ runtime = cls(context)
285
+ return runtime
286
+
287
+ async def __aenter__(self):
288
+ """Async enter method called when entering the 'async with' block.
289
+
290
+ Initializes and prepares the runtime environment.
291
+
292
+ Returns:
293
+ The runtime instance
294
+ """
295
+ # Intercept all stdout/stderr/logs and write them to a file (runtime), stdout (debug)
296
+ self.logs_interceptor = LogsInterceptor(
297
+ min_level=self.context.logs_min_level,
298
+ dir=self.context.runtime_dir,
299
+ file=self.context.logs_file,
300
+ job_id=self.context.job_id,
301
+ )
302
+ self.logs_interceptor.setup()
303
+
304
+ logger.debug(f"Starting runtime with job id: {self.context.job_id}")
305
+
306
+ return self
307
+
308
+ @abstractmethod
309
+ async def execute(self) -> Optional[UiPathRuntimeResult]:
310
+ """Execute with the provided context.
311
+
312
+ Returns:
313
+ Dictionary with execution results
314
+
315
+ Raises:
316
+ RuntimeError: If execution fails
317
+ """
318
+ pass
319
+
320
+ @abstractmethod
321
+ async def validate(self):
322
+ """Validate runtime inputs."""
323
+ pass
324
+
325
+ @abstractmethod
326
+ async def cleanup(self):
327
+ """Cleaup runtime resources."""
328
+ pass
329
+
330
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
331
+ """Async exit method called when exiting the 'async with' block.
332
+
333
+ Cleans up resources and handles any exceptions.
334
+
335
+ Always writes output file regardless of whether execution was successful,
336
+ suspended, or encountered an error.
337
+ """
338
+ try:
339
+ logger.debug(f"Shutting down runtime with job id: {self.context.job_id}")
340
+
341
+ if self.context.result is None:
342
+ execution_result = UiPathRuntimeResult()
343
+ else:
344
+ execution_result = self.context.result
345
+
346
+ if exc_type:
347
+ # Create error info from exception
348
+ if isinstance(exc_val, UiPathRuntimeError):
349
+ error_info = exc_val.error_info
350
+ else:
351
+ # Generic error
352
+ error_info = UiPathErrorContract(
353
+ code=f"ERROR_{exc_type.__name__}",
354
+ title=f"Runtime error: {exc_type.__name__}",
355
+ detail=str(exc_val),
356
+ category=UiPathErrorCategory.UNKNOWN,
357
+ )
358
+
359
+ execution_result.status = UiPathRuntimeStatus.FAULTED
360
+ execution_result.error = error_info
361
+
362
+ content = execution_result.to_dict()
363
+ logger.debug(content)
364
+
365
+ # Always write output file at runtime
366
+ if self.context.job_id:
367
+ with open(self.output_file_path, "w") as f:
368
+ json.dump(content, f, indent=2, default=str)
369
+
370
+ # Don't suppress exceptions
371
+ return False
372
+
373
+ except Exception as e:
374
+ logger.error(f"Error during runtime shutdown: {str(e)}")
375
+
376
+ # Create a fallback error result if we fail during cleanup
377
+ if not isinstance(e, UiPathRuntimeError):
378
+ error_info = UiPathErrorContract(
379
+ code="RUNTIME_SHUTDOWN_ERROR",
380
+ title="Runtime shutdown failed",
381
+ detail=f"Error: {str(e)}",
382
+ category=UiPathErrorCategory.SYSTEM,
383
+ )
384
+ else:
385
+ error_info = e.error_info
386
+
387
+ # Last-ditch effort to write error output
388
+ try:
389
+ error_result = UiPathRuntimeResult(
390
+ status=UiPathRuntimeStatus.FAULTED, error=error_info
391
+ )
392
+ error_result_content = error_result.to_dict()
393
+ logger.debug(error_result_content)
394
+ if self.context.job_id:
395
+ with open(self.output_file_path, "w") as f:
396
+ json.dump(error_result_content, f, indent=2, default=str)
397
+ except Exception as write_error:
398
+ logger.error(f"Failed to write error output file: {str(write_error)}")
399
+ raise
400
+
401
+ # Re-raise as RuntimeError if it's not already a UiPathRuntimeError
402
+ if not isinstance(e, UiPathRuntimeError):
403
+ raise RuntimeError(
404
+ error_info.code,
405
+ error_info.title,
406
+ error_info.detail,
407
+ error_info.category,
408
+ ) from e
409
+ raise
410
+ finally:
411
+ # Restore original logging
412
+ if self.context.job_id and self.logs_interceptor:
413
+ self.logs_interceptor.teardown()
414
+
415
+ await self.cleanup()
416
+
417
+ @cached_property
418
+ def output_file_path(self) -> str:
419
+ if self.context.runtime_dir and self.context.output_file:
420
+ os.makedirs(self.context.runtime_dir, exist_ok=True)
421
+ return os.path.join(self.context.runtime_dir, self.context.output_file)
422
+ return os.path.join("__uipath", "output.json")
423
+
424
+ @cached_property
425
+ def state_file_path(self) -> str:
426
+ if self.context.runtime_dir and self.context.state_file:
427
+ os.makedirs(self.context.runtime_dir, exist_ok=True)
428
+ return os.path.join(self.context.runtime_dir, self.context.state_file)
429
+ return os.path.join("__uipath", "state.db")