uipath 2.1.79__py3-none-any.whl → 2.1.81__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/_cli/_auth/_auth_service.py +89 -109
- uipath/_cli/_auth/_models.py +0 -11
- uipath/_cli/_auth/_oidc_utils.py +30 -1
- uipath/_cli/_auth/_portal_service.py +125 -157
- uipath/_cli/_auth/_url_utils.py +61 -27
- uipath/_cli/_auth/_utils.py +6 -29
- uipath/_cli/_evals/_models/_output.py +17 -14
- uipath/_cli/_evals/_runtime.py +203 -102
- uipath/_cli/_runtime/_runtime.py +3 -1
- uipath/_cli/_utils/_common.py +3 -3
- uipath/_cli/cli_auth.py +2 -2
- uipath/_cli/cli_eval.py +2 -2
- uipath/_services/__init__.py +2 -0
- uipath/{_cli/_auth/_client_credentials.py → _services/external_application_service.py} +49 -54
- uipath/_uipath.py +9 -15
- uipath/_utils/_auth.py +82 -0
- uipath/models/auth.py +14 -0
- {uipath-2.1.79.dist-info → uipath-2.1.81.dist-info}/METADATA +1 -1
- {uipath-2.1.79.dist-info → uipath-2.1.81.dist-info}/RECORD +22 -20
- {uipath-2.1.79.dist-info → uipath-2.1.81.dist-info}/WHEEL +0 -0
- {uipath-2.1.79.dist-info → uipath-2.1.81.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.79.dist-info → uipath-2.1.81.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,24 +1,27 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import time
|
|
3
2
|
from typing import Optional
|
|
4
3
|
|
|
5
4
|
import click
|
|
6
5
|
import httpx
|
|
7
6
|
|
|
7
|
+
from ..._utils._auth import update_env_file
|
|
8
8
|
from ..._utils._ssl_context import get_httpx_client_kwargs
|
|
9
|
+
from ...models.auth import TokenData
|
|
10
|
+
from .._runtime._contracts import UiPathErrorCategory, UiPathRuntimeError
|
|
9
11
|
from .._utils._console import ConsoleLogger
|
|
10
|
-
from ._models import
|
|
12
|
+
from ._models import (
|
|
13
|
+
OrganizationInfo,
|
|
14
|
+
TenantInfo,
|
|
15
|
+
TenantsAndOrganizationInfoResponse,
|
|
16
|
+
)
|
|
11
17
|
from ._oidc_utils import OidcUtils
|
|
12
|
-
from ._url_utils import build_service_url
|
|
18
|
+
from ._url_utils import build_service_url
|
|
13
19
|
from ._utils import (
|
|
14
20
|
get_auth_data,
|
|
15
21
|
get_parsed_token_data,
|
|
16
22
|
update_auth_file,
|
|
17
|
-
update_env_file,
|
|
18
23
|
)
|
|
19
24
|
|
|
20
|
-
console = ConsoleLogger()
|
|
21
|
-
|
|
22
25
|
|
|
23
26
|
class PortalService:
|
|
24
27
|
"""Service for interacting with the UiPath Portal API."""
|
|
@@ -29,7 +32,6 @@ class PortalService:
|
|
|
29
32
|
selected_tenant: Optional[str] = None
|
|
30
33
|
|
|
31
34
|
_client: Optional[httpx.Client] = None
|
|
32
|
-
|
|
33
35
|
_tenants_and_organizations: Optional[TenantsAndOrganizationInfoResponse] = None
|
|
34
36
|
|
|
35
37
|
def __init__(
|
|
@@ -41,8 +43,15 @@ class PortalService:
|
|
|
41
43
|
self.domain = domain
|
|
42
44
|
self.access_token = access_token
|
|
43
45
|
self.prt_id = prt_id
|
|
46
|
+
self._console = ConsoleLogger()
|
|
47
|
+
self._tenants_and_organizations = None
|
|
48
|
+
self._client = None
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
@property
|
|
51
|
+
def client(self) -> httpx.Client:
|
|
52
|
+
if self._client is None:
|
|
53
|
+
self._client = httpx.Client(**get_httpx_client_kwargs())
|
|
54
|
+
return self._client
|
|
46
55
|
|
|
47
56
|
def close(self):
|
|
48
57
|
"""Explicitly close the HTTP client."""
|
|
@@ -59,48 +68,35 @@ class PortalService:
|
|
|
59
68
|
self.close()
|
|
60
69
|
|
|
61
70
|
def update_token_data(self, token_data: TokenData):
|
|
62
|
-
self.access_token = token_data
|
|
71
|
+
self.access_token = token_data.access_token
|
|
63
72
|
self.prt_id = get_parsed_token_data(token_data).get("prt_id")
|
|
73
|
+
update_auth_file(token_data)
|
|
64
74
|
|
|
65
|
-
def get_tenants_and_organizations(
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
def get_tenants_and_organizations(
|
|
76
|
+
self,
|
|
77
|
+
):
|
|
78
|
+
if self._tenants_and_organizations is not None:
|
|
79
|
+
return self._tenants_and_organizations
|
|
68
80
|
|
|
69
81
|
url = build_service_url(
|
|
70
82
|
self.domain,
|
|
71
83
|
f"/{self.prt_id}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo",
|
|
72
84
|
)
|
|
73
|
-
response = self.
|
|
85
|
+
response = self.client.get(
|
|
74
86
|
url, headers={"Authorization": f"Bearer {self.access_token}"}
|
|
75
87
|
)
|
|
76
88
|
if response.status_code < 400:
|
|
77
|
-
|
|
78
|
-
self._tenants_and_organizations
|
|
79
|
-
return result
|
|
80
|
-
elif response.status_code == 401:
|
|
81
|
-
console.error("Unauthorized")
|
|
82
|
-
else:
|
|
83
|
-
console.error(
|
|
84
|
-
f"Failed to get tenants and organizations: {response.status_code} {response.text}"
|
|
85
|
-
)
|
|
86
|
-
# Can't reach here, console.error exits, linting
|
|
87
|
-
raise Exception("Failed to get tenants")
|
|
89
|
+
self._tenants_and_organizations = response.json()
|
|
90
|
+
return self._tenants_and_organizations
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
self._tenants_and_organizations = self.get_tenants_and_organizations()
|
|
92
|
-
organization = self._tenants_and_organizations.get("organization")
|
|
93
|
-
if organization is None:
|
|
94
|
-
console.error("Organization not found.")
|
|
95
|
-
return ""
|
|
96
|
-
account_name = organization.get("name")
|
|
97
|
-
base_url = get_base_url(self.domain)
|
|
98
|
-
return f"{base_url}/{account_name}/{self.selected_tenant}/orchestrator_"
|
|
92
|
+
if response.status_code == 401:
|
|
93
|
+
self._console.error("Unauthorized")
|
|
99
94
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
self._console.error(
|
|
96
|
+
f"Failed to get tenants and organizations: {response.status_code} {response.text}"
|
|
97
|
+
)
|
|
103
98
|
|
|
99
|
+
def refresh_access_token(self, refresh_token: str) -> TokenData: # type: ignore
|
|
104
100
|
url = build_service_url(self.domain, "/identity_/connect/token")
|
|
105
101
|
client_id = OidcUtils.get_auth_config().get("client_id")
|
|
106
102
|
|
|
@@ -111,16 +107,14 @@ class PortalService:
|
|
|
111
107
|
}
|
|
112
108
|
|
|
113
109
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
114
|
-
|
|
115
|
-
response = self._client.post(url, data=data, headers=headers)
|
|
110
|
+
response = self.client.post(url, data=data, headers=headers)
|
|
116
111
|
if response.status_code < 400:
|
|
117
|
-
return response.json()
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
raise Exception("Failed to refresh get token")
|
|
112
|
+
return TokenData.model_validate(response.json())
|
|
113
|
+
|
|
114
|
+
if response.status_code == 401:
|
|
115
|
+
self._console.error("Unauthorized")
|
|
116
|
+
|
|
117
|
+
self._console.error(f"Failed to refresh token: {response.status_code}")
|
|
124
118
|
|
|
125
119
|
def ensure_valid_token(self):
|
|
126
120
|
"""Ensure the access token is valid and refresh it if necessary.
|
|
@@ -136,127 +130,101 @@ class PortalService:
|
|
|
136
130
|
auth_data = get_auth_data()
|
|
137
131
|
claims = get_parsed_token_data(auth_data)
|
|
138
132
|
exp = claims.get("exp")
|
|
139
|
-
if exp is not None and float(exp) > time.time():
|
|
140
|
-
if not os.getenv("UIPATH_URL"):
|
|
141
|
-
tenants_and_organizations = self.get_tenants_and_organizations()
|
|
142
|
-
select_tenant(
|
|
143
|
-
self.domain if self.domain else "alpha", tenants_and_organizations
|
|
144
|
-
)
|
|
145
|
-
updated_env_contents = {
|
|
146
|
-
"UIPATH_ACCESS_TOKEN": auth_data["access_token"],
|
|
147
|
-
}
|
|
148
|
-
else:
|
|
149
|
-
refresh_token = auth_data.get("refresh_token")
|
|
150
|
-
if refresh_token is None:
|
|
151
|
-
raise Exception("Refresh token not found")
|
|
152
|
-
token_data = self.post_refresh_token_request(refresh_token)
|
|
153
|
-
update_auth_file(token_data)
|
|
154
|
-
|
|
155
|
-
self.access_token = token_data["access_token"]
|
|
156
|
-
self.prt_id = claims.get("prt_id")
|
|
157
|
-
|
|
158
|
-
updated_env_contents = {
|
|
159
|
-
"UIPATH_ACCESS_TOKEN": token_data["access_token"],
|
|
160
|
-
}
|
|
161
|
-
if not os.getenv("UIPATH_URL"):
|
|
162
|
-
tenants_and_organizations = self.get_tenants_and_organizations()
|
|
163
|
-
select_tenant(
|
|
164
|
-
self.domain if self.domain else "alpha", tenants_and_organizations
|
|
165
|
-
)
|
|
166
133
|
|
|
167
|
-
|
|
134
|
+
def finalize(token_data: TokenData):
|
|
135
|
+
self.update_token_data(token_data)
|
|
136
|
+
update_env_file({"UIPATH_ACCESS_TOKEN": token_data.access_token})
|
|
168
137
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
138
|
+
if exp is not None and float(exp) > time.time():
|
|
139
|
+
finalize(auth_data)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
refresh_token = auth_data.refresh_token
|
|
143
|
+
if not refresh_token:
|
|
144
|
+
raise UiPathRuntimeError(
|
|
145
|
+
"REFRESH_TOKEN_MISSING",
|
|
146
|
+
"No refresh token found",
|
|
147
|
+
"The refresh token could not be retrieved. Please retry authenticating.",
|
|
148
|
+
UiPathErrorCategory.SYSTEM,
|
|
149
|
+
)
|
|
172
150
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if base_url
|
|
176
|
-
else self.get_uipath_orchestrator_url()
|
|
177
|
-
)
|
|
151
|
+
token_data = self.refresh_access_token(refresh_token)
|
|
152
|
+
finalize(token_data)
|
|
178
153
|
|
|
179
|
-
|
|
180
|
-
|
|
154
|
+
def enable_studio_web(self, base_url: str) -> None:
|
|
155
|
+
or_base_url = self.build_orchestrator_url(base_url)
|
|
181
156
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
headers={"Authorization": f"Bearer {self.access_token}"},
|
|
187
|
-
)
|
|
188
|
-
for url in [url_try_enable_first_run, url_acquire_license]
|
|
189
|
-
]
|
|
190
|
-
except Exception:
|
|
191
|
-
pass
|
|
192
|
-
|
|
193
|
-
def has_initialized_auth(self):
|
|
194
|
-
try:
|
|
195
|
-
auth_data = get_auth_data()
|
|
196
|
-
if not auth_data or "access_token" not in auth_data:
|
|
197
|
-
return False
|
|
198
|
-
if not os.path.exists(".env"):
|
|
199
|
-
return False
|
|
200
|
-
if not os.getenv("UIPATH_ACCESS_TOKEN"):
|
|
201
|
-
return False
|
|
202
|
-
|
|
203
|
-
return True
|
|
204
|
-
except Exception:
|
|
205
|
-
return False
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def select_tenant(
|
|
209
|
-
domain: str, tenants_and_organizations: TenantsAndOrganizationInfoResponse
|
|
210
|
-
):
|
|
211
|
-
tenant_names = [tenant["name"] for tenant in tenants_and_organizations["tenants"]]
|
|
212
|
-
console.display_options(tenant_names, "Select tenant:")
|
|
213
|
-
tenant_idx = (
|
|
214
|
-
0
|
|
215
|
-
if len(tenant_names) == 1
|
|
216
|
-
else console.prompt("Select tenant number", type=int)
|
|
217
|
-
)
|
|
218
|
-
tenant_name = tenant_names[tenant_idx]
|
|
219
|
-
account_name = tenants_and_organizations["organization"]["name"]
|
|
220
|
-
console.info(f"Selected tenant: {click.style(tenant_name, fg='cyan')}")
|
|
221
|
-
|
|
222
|
-
base_url = get_base_url(domain)
|
|
223
|
-
uipath_url = f"{base_url}/{account_name}/{tenant_name}"
|
|
224
|
-
|
|
225
|
-
update_env_file(
|
|
226
|
-
{
|
|
227
|
-
"UIPATH_URL": uipath_url,
|
|
228
|
-
"UIPATH_TENANT_ID": tenants_and_organizations["tenants"][tenant_idx]["id"],
|
|
229
|
-
"UIPATH_ORGANIZATION_ID": tenants_and_organizations["organization"]["id"],
|
|
230
|
-
}
|
|
231
|
-
)
|
|
157
|
+
urls = [
|
|
158
|
+
f"{or_base_url}/api/StudioWeb/TryEnableFirstRun",
|
|
159
|
+
f"{or_base_url}/api/StudioWeb/AcquireLicense",
|
|
160
|
+
]
|
|
232
161
|
|
|
233
|
-
|
|
162
|
+
for url in urls:
|
|
163
|
+
try:
|
|
164
|
+
resp = self.client.post(
|
|
165
|
+
url, headers={"Authorization": f"Bearer {self.access_token}"}
|
|
166
|
+
)
|
|
167
|
+
if resp.status_code >= 400:
|
|
168
|
+
self._console.warning(f"Call to {url} failed: {resp.status_code}")
|
|
169
|
+
except httpx.HTTPError as e:
|
|
170
|
+
self._console.warning(
|
|
171
|
+
f"Exception during enable_studio_web request to {url}: {e}"
|
|
172
|
+
)
|
|
234
173
|
|
|
174
|
+
def _set_tenant(self, tenant: TenantInfo, organization: OrganizationInfo):
|
|
175
|
+
self.selected_tenant = tenant["name"]
|
|
176
|
+
return {"tenant_id": tenant["id"], "organization_id": organization["id"]}
|
|
235
177
|
|
|
236
|
-
def
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
) -> str:
|
|
241
|
-
tenants = tenants_and_organizations["tenants"]
|
|
178
|
+
def _select_tenant(self):
|
|
179
|
+
data = self.get_tenants_and_organizations()
|
|
180
|
+
organization = data["organization"]
|
|
181
|
+
tenants = data["tenants"]
|
|
242
182
|
|
|
243
|
-
|
|
244
|
-
if not matched_tenant:
|
|
245
|
-
console.error(f"Tenant '{tenant_name}' not found.")
|
|
246
|
-
raise ValueError(f"Tenant '{tenant_name}' not found.")
|
|
183
|
+
tenant_names = [tenant["name"] for tenant in tenants]
|
|
247
184
|
|
|
248
|
-
|
|
249
|
-
|
|
185
|
+
self._console.display_options(tenant_names, "Select tenant:")
|
|
186
|
+
tenant_idx = (
|
|
187
|
+
0
|
|
188
|
+
if len(tenant_names) == 1
|
|
189
|
+
else self._console.prompt("Select tenant number", type=int)
|
|
190
|
+
)
|
|
250
191
|
|
|
251
|
-
|
|
252
|
-
uipath_url = f"{base_url}/{account_name}/{tenant_name}"
|
|
192
|
+
tenant = data["tenants"][tenant_idx]
|
|
253
193
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
"UIPATH_URL": uipath_url,
|
|
257
|
-
"UIPATH_TENANT_ID": matched_tenant["id"],
|
|
258
|
-
"UIPATH_ORGANIZATION_ID": tenants_and_organizations["organization"]["id"],
|
|
259
|
-
}
|
|
260
|
-
)
|
|
194
|
+
self._console.info(f"Selected tenant: {click.style(tenant['name'], fg='cyan')}")
|
|
195
|
+
return self._set_tenant(tenant, organization)
|
|
261
196
|
|
|
262
|
-
|
|
197
|
+
def _retrieve_tenant(
|
|
198
|
+
self,
|
|
199
|
+
tenant_name: str,
|
|
200
|
+
):
|
|
201
|
+
data = self.get_tenants_and_organizations()
|
|
202
|
+
organization = data["organization"]
|
|
203
|
+
tenants = data["tenants"]
|
|
204
|
+
|
|
205
|
+
tenant = next((t for t in tenants if t["name"] == tenant_name), None)
|
|
206
|
+
if not tenant:
|
|
207
|
+
self._console.error(f"Tenant '{tenant_name}' not found.")
|
|
208
|
+
|
|
209
|
+
return self._set_tenant(tenant, organization) # type: ignore
|
|
210
|
+
|
|
211
|
+
def resolve_tenant_info(self, tenant: Optional[str] = None):
|
|
212
|
+
if tenant:
|
|
213
|
+
return self._retrieve_tenant(tenant)
|
|
214
|
+
return self._select_tenant()
|
|
215
|
+
|
|
216
|
+
def build_tenant_url(self) -> str:
|
|
217
|
+
data = self.get_tenants_and_organizations()
|
|
218
|
+
organization_name = data["organization"]["name"]
|
|
219
|
+
return f"{self.domain}/{organization_name}/{self.selected_tenant}"
|
|
220
|
+
|
|
221
|
+
def build_orchestrator_url(self, base_url: str) -> str:
|
|
222
|
+
if base_url:
|
|
223
|
+
return f"{base_url}/orchestrator_"
|
|
224
|
+
data = self.get_tenants_and_organizations()
|
|
225
|
+
organization = data.get("organization")
|
|
226
|
+
if organization is None:
|
|
227
|
+
self._console.error("Organization not found.")
|
|
228
|
+
return ""
|
|
229
|
+
organization_name = organization.get("name")
|
|
230
|
+
return f"{self.domain}/{organization_name}/{self.selected_tenant}/orchestrator_"
|
uipath/_cli/_auth/_url_utils.py
CHANGED
|
@@ -1,51 +1,85 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from typing import Optional, Tuple
|
|
2
3
|
from urllib.parse import urlparse
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
from .._utils._console import ConsoleLogger
|
|
5
6
|
|
|
7
|
+
console = ConsoleLogger()
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
def resolve_domain(
|
|
11
|
+
base_url: Optional[str], environment: Optional[str], force: bool = False
|
|
12
|
+
) -> str:
|
|
13
|
+
"""Resolve the UiPath domain, giving priority to base_url when valid.
|
|
9
14
|
|
|
10
15
|
Args:
|
|
11
|
-
|
|
16
|
+
base_url: The base URL explicitly provided.
|
|
17
|
+
environment: The environment name (e.g., 'alpha', 'staging', 'cloud').
|
|
18
|
+
force: Whether to ignore UIPATH_URL from environment variables.
|
|
12
19
|
|
|
13
20
|
Returns:
|
|
14
|
-
|
|
21
|
+
A valid base URL for UiPath services.
|
|
15
22
|
"""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if not ignore_env_var:
|
|
19
|
-
# If UIPATH_URL is set and domain is 'cloud' (default), use the base from UIPATH_URL
|
|
23
|
+
if not force:
|
|
24
|
+
# If UIPATH_URL is set, prefer its domain
|
|
20
25
|
uipath_url = os.getenv("UIPATH_URL")
|
|
21
|
-
if uipath_url and
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
if uipath_url and environment == "cloud":
|
|
27
|
+
parsed = urlparse(uipath_url)
|
|
28
|
+
if parsed.scheme and parsed.netloc:
|
|
29
|
+
domain = f"{parsed.scheme}://{parsed.netloc}"
|
|
30
|
+
return domain
|
|
31
|
+
else:
|
|
32
|
+
console.error(
|
|
33
|
+
f"Malformed UIPATH_URL: '{uipath_url}'. "
|
|
34
|
+
"Please ensure it includes scheme and netloc (e.g., 'https://cloud.uipath.com')."
|
|
35
|
+
)
|
|
31
36
|
|
|
37
|
+
# If base_url is a real URL, prefer it
|
|
38
|
+
if base_url and base_url.startswith("http"):
|
|
39
|
+
parsed = urlparse(base_url)
|
|
40
|
+
domain = f"{parsed.scheme}://{parsed.netloc}"
|
|
41
|
+
if domain:
|
|
42
|
+
return domain
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
ignore_env_var = force
|
|
44
|
+
# Otherwise, fall back to environment
|
|
45
|
+
return f"https://{environment or 'cloud'}.uipath.com"
|
|
36
46
|
|
|
37
47
|
|
|
38
48
|
def build_service_url(domain: str, path: str) -> str:
|
|
39
49
|
"""Build a service URL by combining the base URL with a path.
|
|
40
50
|
|
|
41
51
|
Args:
|
|
42
|
-
domain:
|
|
52
|
+
domain: The domain name
|
|
43
53
|
path: The path to append (should start with /)
|
|
44
54
|
|
|
45
55
|
Returns:
|
|
46
56
|
The complete service URL
|
|
47
57
|
"""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
return f"{domain}{path}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def extract_org_tenant(uipath_url: str) -> Tuple[Optional[str], Optional[str]]:
|
|
62
|
+
"""Extract organization and tenant from a UiPath URL.
|
|
63
|
+
|
|
64
|
+
Accepts values like:
|
|
65
|
+
- https://cloud.uipath.com/myOrg/myTenant
|
|
66
|
+
- https://alpha.uipath.com/myOrg/myTenant/anything_else
|
|
67
|
+
- cloud.uipath.com/myOrg/myTenant (scheme will be assumed https)
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
uipath_url: The UiPath URL to parse
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
A tuple of (organization, tenant) where:
|
|
74
|
+
- organization: 'myOrg' or None
|
|
75
|
+
- tenant: 'myTenant' or None
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
>>> extract_org_tenant('https://cloud.uipath.com/myOrg/myTenant')
|
|
79
|
+
('myOrg', 'myTenant')
|
|
80
|
+
"""
|
|
81
|
+
parsed = urlparse(uipath_url if "://" in uipath_url else f"https://{uipath_url}")
|
|
82
|
+
parts = [p for p in parsed.path.split("/") if p]
|
|
83
|
+
org = parts[0] if len(parts) >= 1 else None
|
|
84
|
+
tenant = parts[1] if len(parts) >= 2 else None
|
|
85
|
+
return org, tenant
|
uipath/_cli/_auth/_utils.py
CHANGED
|
@@ -1,51 +1,28 @@
|
|
|
1
|
-
import base64
|
|
2
1
|
import json
|
|
3
2
|
import os
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from typing import Optional
|
|
6
5
|
|
|
7
|
-
from .
|
|
6
|
+
from ..._utils._auth import parse_access_token
|
|
7
|
+
from ...models.auth import TokenData
|
|
8
|
+
from ._models import AccessTokenData
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def update_auth_file(token_data: TokenData):
|
|
11
12
|
os.makedirs(Path.cwd() / ".uipath", exist_ok=True)
|
|
12
13
|
auth_file = Path.cwd() / ".uipath" / ".auth.json"
|
|
13
14
|
with open(auth_file, "w") as f:
|
|
14
|
-
json.dump(token_data, f)
|
|
15
|
+
json.dump(token_data.model_dump(exclude_none=True), f)
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def get_auth_data() -> TokenData:
|
|
18
19
|
auth_file = Path.cwd() / ".uipath" / ".auth.json"
|
|
19
20
|
if not auth_file.exists():
|
|
20
21
|
raise Exception("No authentication file found")
|
|
21
|
-
return json.load(open(auth_file))
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def parse_access_token(access_token: str) -> AccessTokenData:
|
|
25
|
-
token_parts = access_token.split(".")
|
|
26
|
-
if len(token_parts) < 2:
|
|
27
|
-
raise Exception("Invalid access token")
|
|
28
|
-
payload = base64.urlsafe_b64decode(
|
|
29
|
-
token_parts[1] + "=" * (-len(token_parts[1]) % 4)
|
|
30
|
-
)
|
|
31
|
-
return json.loads(payload)
|
|
22
|
+
return TokenData.model_validate(json.load(open(auth_file)))
|
|
32
23
|
|
|
33
24
|
|
|
34
25
|
def get_parsed_token_data(token_data: Optional[TokenData] = None) -> AccessTokenData:
|
|
35
26
|
if not token_data:
|
|
36
27
|
token_data = get_auth_data()
|
|
37
|
-
return parse_access_token(token_data
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def update_env_file(env_contents):
|
|
41
|
-
env_path = Path.cwd() / ".env"
|
|
42
|
-
if env_path.exists():
|
|
43
|
-
with open(env_path, "r") as f:
|
|
44
|
-
for line in f:
|
|
45
|
-
if "=" in line:
|
|
46
|
-
key, value = line.strip().split("=", 1)
|
|
47
|
-
if key not in env_contents:
|
|
48
|
-
env_contents[key] = value
|
|
49
|
-
lines = [f"{key}={value}\n" for key, value in env_contents.items()]
|
|
50
|
-
with open(env_path, "w") as f:
|
|
51
|
-
f.writelines(lines)
|
|
28
|
+
return parse_access_token(token_data.access_token)
|
|
@@ -58,43 +58,46 @@ class EvaluationRunResultDto(BaseModel):
|
|
|
58
58
|
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
59
59
|
|
|
60
60
|
evaluator_name: str
|
|
61
|
+
evaluator_id: str
|
|
61
62
|
result: EvaluationResultDto
|
|
62
63
|
|
|
64
|
+
@model_serializer(mode="wrap")
|
|
65
|
+
def serialize_model(self, serializer, info):
|
|
66
|
+
data = serializer(self)
|
|
67
|
+
if isinstance(data, dict):
|
|
68
|
+
data.pop("evaluatorId", None)
|
|
69
|
+
return data
|
|
70
|
+
|
|
63
71
|
|
|
64
72
|
class EvaluationRunResult(BaseModel):
|
|
65
73
|
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
66
74
|
|
|
67
|
-
score: float = 0.0
|
|
68
75
|
evaluation_name: str
|
|
69
76
|
evaluation_run_results: List[EvaluationRunResultDto]
|
|
70
77
|
|
|
71
|
-
|
|
78
|
+
@property
|
|
79
|
+
def score(self) -> float:
|
|
72
80
|
"""Compute average score for this single eval_item."""
|
|
73
81
|
if not self.evaluation_run_results:
|
|
74
|
-
|
|
75
|
-
return
|
|
82
|
+
return 0.0
|
|
76
83
|
|
|
77
84
|
total_score = sum(dto.result.score for dto in self.evaluation_run_results)
|
|
78
|
-
|
|
85
|
+
return total_score / len(self.evaluation_run_results)
|
|
79
86
|
|
|
80
87
|
|
|
81
88
|
class UiPathEvalOutput(BaseModel):
|
|
82
89
|
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
83
90
|
|
|
84
91
|
evaluation_set_name: str
|
|
85
|
-
score: float
|
|
86
92
|
evaluation_set_results: List[EvaluationRunResult]
|
|
87
93
|
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
@property
|
|
95
|
+
def score(self) -> float:
|
|
96
|
+
"""Compute overall average score from evaluation results."""
|
|
90
97
|
if not self.evaluation_set_results:
|
|
91
|
-
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
for eval_result in self.evaluation_set_results:
|
|
95
|
-
eval_result.compute_average_score()
|
|
98
|
+
return 0.0
|
|
96
99
|
|
|
97
100
|
eval_item_scores = [
|
|
98
101
|
eval_result.score for eval_result in self.evaluation_set_results
|
|
99
102
|
]
|
|
100
|
-
|
|
103
|
+
return sum(eval_item_scores) / len(eval_item_scores)
|