uipath 2.1.41__py3-none-any.whl → 2.1.43__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.
@@ -7,7 +7,7 @@ import threading
7
7
  import time
8
8
  from typing import Optional
9
9
 
10
- from ._oidc_utils import get_auth_config
10
+ from ._oidc_utils import OidcUtils
11
11
 
12
12
  # Server port
13
13
  PORT = 6234
@@ -74,7 +74,7 @@ def make_request_handler_class(state, code_verifier, token_callback, domain):
74
74
  content = f.read()
75
75
 
76
76
  # Get the redirect URI from auth config
77
- auth_config = get_auth_config()
77
+ auth_config = OidcUtils.get_auth_config()
78
78
  redirect_uri = auth_config["redirect_uri"]
79
79
 
80
80
  content = content.replace("__PY_REPLACE_EXPECTED_STATE__", state)
@@ -0,0 +1,151 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import webbrowser
5
+ from socket import AF_INET, SOCK_STREAM, error, socket
6
+ from typing import Optional
7
+ from urllib.parse import urlparse
8
+
9
+ from uipath._cli._auth._auth_server import HTTPServer
10
+ from uipath._cli._auth._client_credentials import ClientCredentialsService
11
+ from uipath._cli._auth._oidc_utils import OidcUtils
12
+ from uipath._cli._auth._portal_service import PortalService, select_tenant
13
+ from uipath._cli._auth._url_utils import set_force_flag
14
+ from uipath._cli._auth._utils import update_auth_file, update_env_file
15
+ from uipath._cli._utils._console import ConsoleLogger
16
+
17
+
18
+ class AuthService:
19
+ def __init__(
20
+ self,
21
+ environment: str,
22
+ *,
23
+ force: bool,
24
+ client_id: Optional[str],
25
+ client_secret: Optional[str],
26
+ base_url: Optional[str],
27
+ scope: Optional[str],
28
+ ):
29
+ self._force = force
30
+ self._console = ConsoleLogger()
31
+ self._domain = self._get_domain(environment)
32
+ self._client_id = client_id
33
+ self._client_secret = client_secret
34
+ self._base_url = base_url
35
+ self._scope = scope
36
+ set_force_flag(self._force)
37
+
38
+ def _get_domain(self, environment: str) -> str:
39
+ # only search env var if not force authentication
40
+ if not self._force:
41
+ uipath_url = os.getenv("UIPATH_URL")
42
+ if uipath_url and environment == "cloud": # "cloud" is the default
43
+ parsed_url = urlparse(uipath_url)
44
+ if parsed_url.scheme and parsed_url.netloc:
45
+ environment = f"{parsed_url.scheme}://{parsed_url.netloc}"
46
+ else:
47
+ self._console.error(
48
+ f"Malformed UIPATH_URL: '{uipath_url}'. Please ensure it includes both scheme and netloc (e.g., 'https://cloud.uipath.com')."
49
+ )
50
+ return environment
51
+
52
+ def authenticate(self) -> None:
53
+ if self._client_id and self._client_secret:
54
+ self._authenticate_client_credentials()
55
+ return
56
+
57
+ self._authenticate_authorization_code()
58
+
59
+ def _authenticate_client_credentials(self) -> None:
60
+ if not self._base_url:
61
+ self._console.error(
62
+ "--base-url is required when using client credentials authentication."
63
+ )
64
+ return
65
+ self._console.hint("Using client credentials authentication.")
66
+ credentials_service = ClientCredentialsService(self._base_url)
67
+ credentials_service.authenticate(
68
+ self._client_id, # type: ignore
69
+ self._client_secret, # type: ignore
70
+ self._scope,
71
+ )
72
+
73
+ def _authenticate_authorization_code(self) -> None:
74
+ with PortalService(self._domain) as portal_service:
75
+ if not self._force:
76
+ # use existing env vars
77
+ if (
78
+ os.getenv("UIPATH_URL")
79
+ and os.getenv("UIPATH_TENANT_ID")
80
+ and os.getenv("UIPATH_ORGANIZATION_ID")
81
+ ):
82
+ try:
83
+ portal_service.ensure_valid_token()
84
+ return
85
+ except Exception:
86
+ self._console.error(
87
+ "Authentication token is invalid. Please reauthenticate using the '--force' flag.",
88
+ )
89
+ auth_url, code_verifier, state = OidcUtils.get_auth_url(self._domain)
90
+ webbrowser.open(auth_url, 1)
91
+ auth_config = OidcUtils.get_auth_config()
92
+
93
+ self._console.link(
94
+ "If a browser window did not open, please open the following URL in your browser:",
95
+ auth_url,
96
+ )
97
+ server = HTTPServer(port=auth_config["port"])
98
+ token_data = asyncio.run(server.start(state, code_verifier, self._domain))
99
+
100
+ if not token_data:
101
+ self._console.error(
102
+ "Authentication failed. Please try again.",
103
+ )
104
+
105
+ portal_service.update_token_data(token_data)
106
+ update_auth_file(token_data)
107
+ access_token = token_data["access_token"]
108
+ update_env_file({"UIPATH_ACCESS_TOKEN": access_token})
109
+
110
+ tenants_and_organizations = portal_service.get_tenants_and_organizations()
111
+ base_url = select_tenant(self._domain, tenants_and_organizations)
112
+ try:
113
+ portal_service.post_auth(base_url)
114
+ except Exception:
115
+ self._console.error(
116
+ "Could not prepare the environment. Please try again.",
117
+ )
118
+
119
+ def set_port(self):
120
+ def is_port_in_use(target_port: int) -> bool:
121
+ with socket(AF_INET, SOCK_STREAM) as s:
122
+ try:
123
+ s.bind(("localhost", target_port))
124
+ s.close()
125
+ return False
126
+ except error:
127
+ return True
128
+
129
+ auth_config = OidcUtils.get_auth_config()
130
+ port = int(auth_config.get("port", 8104))
131
+ port_option_one = int(auth_config.get("portOptionOne", 8104)) # type: ignore
132
+ port_option_two = int(auth_config.get("portOptionTwo", 8055)) # type: ignore
133
+ port_option_three = int(auth_config.get("portOptionThree", 42042)) # type: ignore
134
+ if is_port_in_use(port):
135
+ if is_port_in_use(port_option_one):
136
+ if is_port_in_use(port_option_two):
137
+ if is_port_in_use(port_option_three):
138
+ self._console.error(
139
+ "All configured ports are in use. Please close applications using ports or configure different ports."
140
+ )
141
+ else:
142
+ port = port_option_three
143
+ else:
144
+ port = port_option_two
145
+ else:
146
+ port = port_option_one
147
+ auth_config["port"] = port
148
+ with open(
149
+ os.path.join(os.path.dirname(__file__), "..", "auth_config.json"), "w"
150
+ ) as f:
151
+ json.dump(auth_config, f)
@@ -1,4 +1,4 @@
1
- from typing import Optional
1
+ from typing import Optional, cast
2
2
  from urllib.parse import urlparse
3
3
 
4
4
  import httpx
@@ -14,12 +14,13 @@ console = ConsoleLogger()
14
14
  class ClientCredentialsService:
15
15
  """Service for client credentials authentication flow."""
16
16
 
17
- def __init__(self, domain: str):
18
- self.domain = domain
17
+ def __init__(self, base_url: str):
18
+ self._base_url = base_url
19
+ self._domain = self._extract_domain_from_base_url(base_url)
19
20
 
20
21
  def get_token_url(self) -> str:
21
22
  """Get the token URL for the specified domain."""
22
- match self.domain:
23
+ match self._domain:
23
24
  case "alpha":
24
25
  return "https://alpha.uipath.com/identity_/connect/token"
25
26
  case "staging":
@@ -39,7 +40,7 @@ class ClientCredentialsService:
39
40
  """
40
41
  return hostname == domain or hostname.endswith(f".{domain}")
41
42
 
42
- def extract_domain_from_base_url(self, base_url: str) -> str:
43
+ def _extract_domain_from_base_url(self, base_url: str) -> str:
43
44
  """Extract domain from base URL.
44
45
 
45
46
  Args:
@@ -71,7 +72,7 @@ class ClientCredentialsService:
71
72
 
72
73
  def authenticate(
73
74
  self, client_id: str, client_secret: str, scope: Optional[str] = "OR.Execution"
74
- ) -> Optional[TokenData]:
75
+ ) -> None:
75
76
  """Authenticate using client credentials flow.
76
77
 
77
78
  Args:
@@ -97,37 +98,35 @@ class ClientCredentialsService:
97
98
  match response.status_code:
98
99
  case 200:
99
100
  token_data = response.json()
100
- return {
101
- "access_token": token_data["access_token"],
102
- "token_type": token_data.get("token_type", "Bearer"),
103
- "expires_in": token_data.get("expires_in", 3600),
104
- "scope": token_data.get("scope", scope),
105
- # Client credentials flow doesn't provide these, but we need them for compatibility
106
- "refresh_token": "",
107
- "id_token": "",
108
- }
101
+ token_data = cast(
102
+ TokenData,
103
+ {
104
+ "access_token": token_data["access_token"],
105
+ "token_type": token_data.get("token_type", "Bearer"),
106
+ "expires_in": token_data.get("expires_in", 3600),
107
+ "scope": token_data.get("scope", scope),
108
+ "refresh_token": "",
109
+ "id_token": "",
110
+ },
111
+ )
112
+ self._setup_environment(token_data)
109
113
  case 400:
110
114
  console.error(
111
115
  "Invalid client credentials or request parameters."
112
116
  )
113
- return None
114
117
  case 401:
115
118
  console.error("Unauthorized: Invalid client credentials.")
116
- return None
117
119
  case _:
118
120
  console.error(
119
121
  f"Authentication failed: {response.status_code} - {response.text}"
120
122
  )
121
- return None
122
123
 
123
124
  except httpx.RequestError as e:
124
125
  console.error(f"Network error during authentication: {e}")
125
- return None
126
126
  except Exception as e:
127
127
  console.error(f"Unexpected error during authentication: {e}")
128
- return None
129
128
 
130
- def setup_environment(self, token_data: TokenData, base_url: str):
129
+ def _setup_environment(self, token_data: TokenData):
131
130
  """Setup environment variables for client credentials authentication.
132
131
 
133
132
  Args:
@@ -138,7 +137,7 @@ class ClientCredentialsService:
138
137
 
139
138
  env_vars = {
140
139
  "UIPATH_ACCESS_TOKEN": token_data["access_token"],
141
- "UIPATH_URL": base_url,
140
+ "UIPATH_URL": self._base_url,
142
141
  "UIPATH_ORGANIZATION_ID": parsed_access_token.get("prt_id", ""),
143
142
  "UIPATH_TENANT_ID": "",
144
143
  }
@@ -24,47 +24,52 @@ def get_state_param() -> str:
24
24
  return base64.urlsafe_b64encode(os.urandom(32)).decode("utf-8").rstrip("=")
25
25
 
26
26
 
27
- def get_auth_config() -> AuthConfig:
28
- auth_config = {}
29
- with open(os.path.join(os.path.dirname(__file__), "auth_config.json"), "r") as f:
30
- auth_config = json.load(f)
31
-
32
- port = auth_config.get("port", 8104)
33
-
34
- redirect_uri = auth_config["redirect_uri"].replace("__PY_REPLACE_PORT__", str(port))
35
-
36
- return AuthConfig(
37
- client_id=auth_config["client_id"],
38
- redirect_uri=redirect_uri,
39
- scope=auth_config["scope"],
40
- port=port,
41
- )
42
-
43
-
44
- def get_auth_url(domain: str) -> tuple[str, str, str]:
45
- """Get the authorization URL for OAuth2 PKCE flow.
46
-
47
- Args:
48
- domain (str): The UiPath domain to authenticate against (e.g. 'alpha', 'cloud')
49
-
50
- Returns:
51
- tuple[str, str]: A tuple containing:
52
- - The authorization URL with query parameters
53
- - The code verifier for PKCE flow
54
- """
55
- code_verifier, code_challenge = generate_code_verifier_and_challenge()
56
- auth_config = get_auth_config()
57
- state = get_state_param()
58
- query_params = {
59
- "client_id": auth_config["client_id"],
60
- "redirect_uri": auth_config["redirect_uri"],
61
- "response_type": "code",
62
- "scope": auth_config["scope"],
63
- "state": state,
64
- "code_challenge": code_challenge,
65
- "code_challenge_method": "S256",
66
- }
67
-
68
- query_string = urlencode(query_params)
69
- url = build_service_url(domain, f"/identity_/connect/authorize?{query_string}")
70
- return url, code_verifier, state
27
+ class OidcUtils:
28
+ @classmethod
29
+ def get_auth_config(cls) -> AuthConfig:
30
+ with open(
31
+ os.path.join(os.path.dirname(__file__), "auth_config.json"), "r"
32
+ ) as f:
33
+ auth_config = json.load(f)
34
+
35
+ port = auth_config.get("port", 8104)
36
+
37
+ redirect_uri = auth_config["redirect_uri"].replace(
38
+ "__PY_REPLACE_PORT__", str(port)
39
+ )
40
+
41
+ return AuthConfig(
42
+ client_id=auth_config["client_id"],
43
+ redirect_uri=redirect_uri,
44
+ scope=auth_config["scope"],
45
+ port=port,
46
+ )
47
+
48
+ @classmethod
49
+ def get_auth_url(cls, domain: str) -> tuple[str, str, str]:
50
+ """Get the authorization URL for OAuth2 PKCE flow.
51
+
52
+ Args:
53
+ domain (str): The UiPath domain to authenticate against (e.g. 'alpha', 'cloud')
54
+
55
+ Returns:
56
+ tuple[str, str]: A tuple containing:
57
+ - The authorization URL with query parameters
58
+ - The code verifier for PKCE flow
59
+ """
60
+ code_verifier, code_challenge = generate_code_verifier_and_challenge()
61
+ auth_config = cls.get_auth_config()
62
+ state = get_state_param()
63
+ query_params = {
64
+ "client_id": auth_config["client_id"],
65
+ "redirect_uri": auth_config["redirect_uri"],
66
+ "response_type": "code",
67
+ "scope": auth_config["scope"],
68
+ "state": state,
69
+ "code_challenge": code_challenge,
70
+ "code_challenge_method": "S256",
71
+ }
72
+
73
+ query_string = urlencode(query_params)
74
+ url = build_service_url(domain, f"/identity_/connect/authorize?{query_string}")
75
+ return url, code_verifier, state
@@ -8,7 +8,7 @@ import httpx
8
8
  from ..._utils._ssl_context import get_httpx_client_kwargs
9
9
  from .._utils._console import ConsoleLogger
10
10
  from ._models import TenantsAndOrganizationInfoResponse, TokenData
11
- from ._oidc_utils import get_auth_config
11
+ from ._oidc_utils import OidcUtils
12
12
  from ._url_utils import build_service_url, get_base_url
13
13
  from ._utils import (
14
14
  get_auth_data,
@@ -102,7 +102,7 @@ class PortalService:
102
102
  raise RuntimeError("HTTP client is not initialized")
103
103
 
104
104
  url = build_service_url(self.domain, "/identity_/connect/token")
105
- client_id = get_auth_config().get("client_id")
105
+ client_id = OidcUtils.get_auth_config().get("client_id")
106
106
 
107
107
  data = {
108
108
  "grant_type": "refresh_token",
@@ -136,32 +136,33 @@ class PortalService:
136
136
  auth_data = get_auth_data()
137
137
  claims = get_parsed_token_data(auth_data)
138
138
  exp = claims.get("exp")
139
-
140
139
  if exp is not None and float(exp) > time.time():
141
140
  if not os.getenv("UIPATH_URL"):
142
141
  tenants_and_organizations = self.get_tenants_and_organizations()
143
142
  select_tenant(
144
143
  self.domain if self.domain else "alpha", tenants_and_organizations
145
144
  )
146
- return auth_data.get("access_token")
147
-
148
- refresh_token = auth_data.get("refresh_token")
149
- if refresh_token is None:
150
- raise Exception("Refresh token not found")
151
- token_data = self.post_refresh_token_request(refresh_token)
152
- update_auth_file(token_data)
153
-
154
- self.access_token = token_data["access_token"]
155
- self.prt_id = claims.get("prt_id")
156
-
157
- updated_env_contents = {
158
- "UIPATH_ACCESS_TOKEN": token_data["access_token"],
159
- }
160
- if not os.getenv("UIPATH_URL"):
161
- tenants_and_organizations = self.get_tenants_and_organizations()
162
- select_tenant(
163
- self.domain if self.domain else "alpha", tenants_and_organizations
164
- )
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
+ )
165
166
 
166
167
  update_env_file(updated_env_contents)
167
168
 
@@ -1,6 +1,8 @@
1
1
  import os
2
2
  from urllib.parse import urlparse
3
3
 
4
+ ignore_env_var = False
5
+
4
6
 
5
7
  def get_base_url(domain: str) -> str:
6
8
  """Get the base URL for UiPath services.
@@ -11,11 +13,14 @@ def get_base_url(domain: str) -> str:
11
13
  Returns:
12
14
  The base URL to use for UiPath services
13
15
  """
14
- # If UIPATH_URL is set and domain is 'cloud' (default), use the base from UIPATH_URL
15
- uipath_url = os.getenv("UIPATH_URL")
16
- if uipath_url and domain == "cloud":
17
- parsed_url = urlparse(uipath_url)
18
- return f"{parsed_url.scheme}://{parsed_url.netloc}"
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
20
+ 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}"
19
24
 
20
25
  # If domain is already a full URL, use it directly
21
26
  if domain.startswith("http"):
@@ -25,6 +30,11 @@ def get_base_url(domain: str) -> str:
25
30
  return f"https://{domain if domain else 'cloud'}.uipath.com"
26
31
 
27
32
 
33
+ def set_force_flag(force: bool):
34
+ global ignore_env_var
35
+ ignore_env_var = force
36
+
37
+
28
38
  def build_service_url(domain: str, path: str) -> str:
29
39
  """Build a service URL by combining the base URL with a path.
30
40
 
@@ -189,6 +189,8 @@ class UiPathDevTerminal(App[Any]):
189
189
 
190
190
  if not run.conversational:
191
191
  asyncio.create_task(self._execute_runtime(run))
192
+ else:
193
+ self._focus_chat_input()
192
194
 
193
195
  async def action_clear_history(self) -> None:
194
196
  """Clear run history."""
@@ -285,6 +287,13 @@ class UiPathDevTerminal(App[Any]):
285
287
  # Populate the details panel with run data
286
288
  details_panel.update_run(run)
287
289
 
290
+ def _focus_chat_input(self):
291
+ """Focus the chat input box."""
292
+ details_panel = self.query_one("#details-panel", RunDetailsPanel)
293
+ details_panel.switch_tab("chat-tab")
294
+ chat_input = details_panel.query_one("#chat-input", Input)
295
+ chat_input.focus()
296
+
288
297
  def _add_run_in_history(self, run: ExecutionRun):
289
298
  """Add run to history panel."""
290
299
  history_panel = self.query_one("#history-panel", RunHistoryPanel)
@@ -292,7 +292,11 @@ class RunDetailsPanel(Container):
292
292
  def _rebuild_spans_tree(self):
293
293
  """Rebuild the spans tree from current run's traces."""
294
294
  spans_tree = self.query_one("#spans-tree", Tree)
295
- spans_tree.clear()
295
+ if spans_tree is None or spans_tree.root is None:
296
+ return
297
+
298
+ spans_tree.root.remove_children()
299
+
296
300
  # Only clear the node mapping since we're rebuilding the tree structure
297
301
  self.span_tree_nodes.clear()
298
302
 
@@ -0,0 +1,14 @@
1
+ from opentelemetry.sdk.trace import ReadableSpan
2
+ from pydantic import BaseModel, ConfigDict
3
+
4
+ from uipath._cli._runtime._contracts import UiPathRuntimeResult
5
+
6
+
7
+ class UiPathEvalRunExecutionOutput(BaseModel):
8
+ """Result of a single agent response."""
9
+
10
+ model_config = ConfigDict(arbitrary_types_allowed=True)
11
+
12
+ execution_time: float
13
+ spans: list[ReadableSpan]
14
+ result: UiPathRuntimeResult