uipath 2.0.0.dev2__py3-none-any.whl → 2.0.1__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.
- uipath/__init__.py +24 -0
- uipath/_cli/README.md +11 -0
- uipath/_cli/__init__.py +54 -0
- uipath/_cli/_auth/_auth_server.py +165 -0
- uipath/_cli/_auth/_models.py +51 -0
- uipath/_cli/_auth/_oidc_utils.py +69 -0
- uipath/_cli/_auth/_portal_service.py +163 -0
- uipath/_cli/_auth/_utils.py +51 -0
- uipath/_cli/_auth/auth_config.json +6 -0
- uipath/_cli/_auth/index.html +167 -0
- uipath/_cli/_auth/localhost.crt +25 -0
- uipath/_cli/_auth/localhost.key +27 -0
- uipath/_cli/_runtime/_contracts.py +429 -0
- uipath/_cli/_runtime/_logging.py +193 -0
- uipath/_cli/_runtime/_runtime.py +264 -0
- uipath/_cli/_templates/.psmdcp.template +9 -0
- uipath/_cli/_templates/.rels.template +5 -0
- uipath/_cli/_templates/[Content_Types].xml.template +9 -0
- uipath/_cli/_templates/main.py.template +25 -0
- uipath/_cli/_templates/package.nuspec.template +10 -0
- uipath/_cli/_utils/_common.py +24 -0
- uipath/_cli/_utils/_input_args.py +126 -0
- uipath/_cli/_utils/_parse_ast.py +542 -0
- uipath/_cli/cli_auth.py +97 -0
- uipath/_cli/cli_deploy.py +13 -0
- uipath/_cli/cli_init.py +113 -0
- uipath/_cli/cli_new.py +76 -0
- uipath/_cli/cli_pack.py +337 -0
- uipath/_cli/cli_publish.py +113 -0
- uipath/_cli/cli_run.py +133 -0
- uipath/_cli/middlewares.py +113 -0
- uipath/_config.py +6 -0
- uipath/_execution_context.py +83 -0
- uipath/_folder_context.py +62 -0
- uipath/_models/__init__.py +37 -0
- uipath/_models/action_schema.py +26 -0
- uipath/_models/actions.py +64 -0
- uipath/_models/assets.py +48 -0
- uipath/_models/connections.py +51 -0
- uipath/_models/context_grounding.py +18 -0
- uipath/_models/context_grounding_index.py +60 -0
- uipath/_models/exceptions.py +6 -0
- uipath/_models/interrupt_models.py +28 -0
- uipath/_models/job.py +66 -0
- uipath/_models/llm_gateway.py +101 -0
- uipath/_models/processes.py +48 -0
- uipath/_models/queues.py +167 -0
- uipath/_services/__init__.py +26 -0
- uipath/_services/_base_service.py +250 -0
- uipath/_services/actions_service.py +271 -0
- uipath/_services/api_client.py +89 -0
- uipath/_services/assets_service.py +257 -0
- uipath/_services/buckets_service.py +268 -0
- uipath/_services/connections_service.py +185 -0
- uipath/_services/connections_service.pyi +50 -0
- uipath/_services/context_grounding_service.py +402 -0
- uipath/_services/folder_service.py +49 -0
- uipath/_services/jobs_service.py +265 -0
- uipath/_services/llm_gateway_service.py +311 -0
- uipath/_services/processes_service.py +168 -0
- uipath/_services/queues_service.py +314 -0
- uipath/_uipath.py +98 -0
- uipath/_utils/__init__.py +17 -0
- uipath/_utils/_endpoint.py +79 -0
- uipath/_utils/_infer_bindings.py +30 -0
- uipath/_utils/_logs.py +15 -0
- uipath/_utils/_request_override.py +18 -0
- uipath/_utils/_request_spec.py +23 -0
- uipath/_utils/_user_agent.py +16 -0
- uipath/_utils/constants.py +25 -0
- uipath/py.typed +0 -0
- {uipath-2.0.0.dev2.dist-info → uipath-2.0.1.dist-info}/METADATA +2 -3
- uipath-2.0.1.dist-info/RECORD +75 -0
- uipath-2.0.0.dev2.dist-info/RECORD +0 -4
- {uipath-2.0.0.dev2.dist-info → uipath-2.0.1.dist-info}/WHEEL +0 -0
- {uipath-2.0.0.dev2.dist-info → uipath-2.0.1.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")
|