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.

@@ -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 TenantsAndOrganizationInfoResponse, TokenData
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, get_base_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
- self._client = httpx.Client(**get_httpx_client_kwargs())
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["access_token"]
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(self) -> TenantsAndOrganizationInfoResponse:
66
- if self._client is None:
67
- raise RuntimeError("HTTP client is not initialized")
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._client.get(
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
- result = response.json()
78
- self._tenants_and_organizations = result
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
- def get_uipath_orchestrator_url(self) -> str:
90
- if self._tenants_and_organizations is None:
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
- def post_refresh_token_request(self, refresh_token: str) -> TokenData:
101
- if self._client is None:
102
- raise RuntimeError("HTTP client is not initialized")
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
- elif response.status_code == 401:
119
- console.error("Unauthorized")
120
- else:
121
- console.error(f"Failed to refresh token: {response.status_code}")
122
- # Can't reach here, console.error exits, linting
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
- update_env_file(updated_env_contents)
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
- def post_auth(self, base_url: str) -> None:
170
- if self._client is None:
171
- raise RuntimeError("HTTP client is not initialized")
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
- or_base_url = (
174
- f"{base_url}/orchestrator_"
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
- url_try_enable_first_run = f"{or_base_url}/api/StudioWeb/TryEnableFirstRun"
180
- url_acquire_license = f"{or_base_url}/api/StudioWeb/AcquireLicense"
154
+ def enable_studio_web(self, base_url: str) -> None:
155
+ or_base_url = self.build_orchestrator_url(base_url)
181
156
 
182
- try:
183
- [try_enable_first_run_response, acquire_license_response] = [
184
- self._client.post(
185
- url,
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
- return uipath_url
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 get_tenant_id(
237
- domain: str,
238
- tenant_name: str,
239
- tenants_and_organizations: TenantsAndOrganizationInfoResponse,
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
- matched_tenant = next((t for t in tenants if t["name"] == tenant_name), None)
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
- domain = get_base_url(domain)
249
- account_name = tenants_and_organizations["organization"]["name"]
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
- base_url = get_base_url(domain)
252
- uipath_url = f"{base_url}/{account_name}/{tenant_name}"
192
+ tenant = data["tenants"][tenant_idx]
253
193
 
254
- update_env_file(
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
- return uipath_url
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_"
@@ -1,51 +1,85 @@
1
1
  import os
2
+ from typing import Optional, Tuple
2
3
  from urllib.parse import urlparse
3
4
 
4
- ignore_env_var = False
5
+ from .._utils._console import ConsoleLogger
5
6
 
7
+ console = ConsoleLogger()
6
8
 
7
- def get_base_url(domain: str) -> str:
8
- """Get the base URL for UiPath services.
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
- domain: Either a domain name (e.g., 'cloud', 'alpha') or a full URL from UIPATH_URL
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
- The base URL to use for UiPath services
21
+ A valid base URL for UiPath services.
15
22
  """
16
- global ignore_env_var
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 domain == "cloud":
22
- parsed_url = urlparse(uipath_url)
23
- return f"{parsed_url.scheme}://{parsed_url.netloc}"
24
-
25
- # If domain is already a full URL, use it directly
26
- if domain.startswith("http"):
27
- return domain
28
-
29
- # Otherwise, construct the URL using the domain
30
- return f"https://{domain if domain else 'cloud'}.uipath.com"
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
- def set_force_flag(force: bool):
34
- global ignore_env_var
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: Either a domain name or full URL
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
- base_url = get_base_url(domain)
49
- # Remove trailing slash from base_url to avoid double slashes
50
- base_url = base_url.rstrip("/")
51
- return f"{base_url}{path}"
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
@@ -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 ._models import AccessTokenData, TokenData
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["access_token"])
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
- def compute_average_score(self) -> None:
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
- self.score = 0.0
75
- return
82
+ return 0.0
76
83
 
77
84
  total_score = sum(dto.result.score for dto in self.evaluation_run_results)
78
- self.score = total_score / len(self.evaluation_run_results)
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
- def compute_average_score(self) -> None:
89
- """Compute overall average by calling eval_item.compute_average_score()."""
94
+ @property
95
+ def score(self) -> float:
96
+ """Compute overall average score from evaluation results."""
90
97
  if not self.evaluation_set_results:
91
- self.score = 0.0
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
- self.score = sum(eval_item_scores) / len(eval_item_scores)
103
+ return sum(eval_item_scores) / len(eval_item_scores)