uipath 2.1.80__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/_utils/_common.py +3 -3
- uipath/_cli/cli_auth.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.80.dist-info → uipath-2.1.81.dist-info}/METADATA +1 -1
- {uipath-2.1.80.dist-info → uipath-2.1.81.dist-info}/RECORD +18 -16
- {uipath-2.1.80.dist-info → uipath-2.1.81.dist-info}/WHEEL +0 -0
- {uipath-2.1.80.dist-info → uipath-2.1.81.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.80.dist-info → uipath-2.1.81.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import json
|
|
3
2
|
import os
|
|
4
3
|
import webbrowser
|
|
5
|
-
from socket import AF_INET, SOCK_STREAM, error, socket
|
|
6
4
|
from typing import Optional
|
|
7
|
-
from urllib.parse import urlparse
|
|
8
5
|
|
|
9
6
|
from uipath._cli._auth._auth_server import HTTPServer
|
|
10
|
-
from uipath._cli._auth._client_credentials import ClientCredentialsService
|
|
11
7
|
from uipath._cli._auth._oidc_utils import OidcUtils
|
|
12
8
|
from uipath._cli._auth._portal_service import (
|
|
13
9
|
PortalService,
|
|
14
|
-
get_tenant_id,
|
|
15
|
-
select_tenant,
|
|
16
10
|
)
|
|
17
|
-
from uipath._cli._auth._url_utils import
|
|
18
|
-
from uipath._cli._auth._utils import
|
|
11
|
+
from uipath._cli._auth._url_utils import extract_org_tenant, resolve_domain
|
|
12
|
+
from uipath._cli._auth._utils import get_parsed_token_data
|
|
19
13
|
from uipath._cli._utils._console import ConsoleLogger
|
|
14
|
+
from uipath._services import ExternalApplicationService
|
|
15
|
+
from uipath._utils._auth import update_env_file
|
|
16
|
+
from uipath.models.auth import TokenData
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
class AuthService:
|
|
@@ -25,35 +22,20 @@ class AuthService:
|
|
|
25
22
|
environment: str,
|
|
26
23
|
*,
|
|
27
24
|
force: bool,
|
|
28
|
-
client_id: Optional[str],
|
|
29
|
-
client_secret: Optional[str],
|
|
30
|
-
base_url: Optional[str],
|
|
31
|
-
tenant: Optional[str],
|
|
32
|
-
scope: Optional[str],
|
|
25
|
+
client_id: Optional[str] = None,
|
|
26
|
+
client_secret: Optional[str] = None,
|
|
27
|
+
base_url: Optional[str] = None,
|
|
28
|
+
tenant: Optional[str] = None,
|
|
29
|
+
scope: Optional[str] = None,
|
|
33
30
|
):
|
|
34
31
|
self._force = force
|
|
35
32
|
self._console = ConsoleLogger()
|
|
36
|
-
self._domain = self._get_domain(environment)
|
|
37
33
|
self._client_id = client_id
|
|
38
34
|
self._client_secret = client_secret
|
|
39
35
|
self._base_url = base_url
|
|
40
36
|
self._tenant = tenant
|
|
37
|
+
self._domain = resolve_domain(self._base_url, environment, self._force)
|
|
41
38
|
self._scope = scope
|
|
42
|
-
set_force_flag(self._force)
|
|
43
|
-
|
|
44
|
-
def _get_domain(self, environment: str) -> str:
|
|
45
|
-
# only search env var if not force authentication
|
|
46
|
-
if not self._force:
|
|
47
|
-
uipath_url = os.getenv("UIPATH_URL")
|
|
48
|
-
if uipath_url and environment == "cloud": # "cloud" is the default
|
|
49
|
-
parsed_url = urlparse(uipath_url)
|
|
50
|
-
if parsed_url.scheme and parsed_url.netloc:
|
|
51
|
-
environment = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
|
52
|
-
else:
|
|
53
|
-
self._console.error(
|
|
54
|
-
f"Malformed UIPATH_URL: '{uipath_url}'. Please ensure it includes both scheme and netloc (e.g., 'https://cloud.uipath.com')."
|
|
55
|
-
)
|
|
56
|
-
return environment
|
|
57
39
|
|
|
58
40
|
def authenticate(self) -> None:
|
|
59
41
|
if self._client_id and self._client_secret:
|
|
@@ -62,103 +44,101 @@ class AuthService:
|
|
|
62
44
|
|
|
63
45
|
self._authenticate_authorization_code()
|
|
64
46
|
|
|
65
|
-
def _authenticate_client_credentials(self)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"--base-url is required when using client credentials authentication."
|
|
69
|
-
)
|
|
70
|
-
return
|
|
71
|
-
self._console.hint("Using client credentials authentication.")
|
|
72
|
-
credentials_service = ClientCredentialsService(self._base_url)
|
|
73
|
-
credentials_service.authenticate(
|
|
47
|
+
def _authenticate_client_credentials(self):
|
|
48
|
+
external_app_service = ExternalApplicationService(self._base_url)
|
|
49
|
+
token_data = external_app_service.get_token_data(
|
|
74
50
|
self._client_id, # type: ignore
|
|
75
51
|
self._client_secret, # type: ignore
|
|
76
52
|
self._scope,
|
|
77
53
|
)
|
|
78
54
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
and os.getenv("UIPATH_ORGANIZATION_ID")
|
|
87
|
-
):
|
|
88
|
-
try:
|
|
89
|
-
portal_service.ensure_valid_token()
|
|
90
|
-
return
|
|
91
|
-
except Exception:
|
|
92
|
-
self._console.error(
|
|
93
|
-
"Authentication token is invalid. Please reauthenticate using the '--force' flag.",
|
|
94
|
-
)
|
|
95
|
-
auth_url, code_verifier, state = OidcUtils.get_auth_url(self._domain)
|
|
96
|
-
webbrowser.open(auth_url, 1)
|
|
97
|
-
auth_config = OidcUtils.get_auth_config()
|
|
98
|
-
|
|
99
|
-
self._console.link(
|
|
100
|
-
"If a browser window did not open, please open the following URL in your browser:",
|
|
101
|
-
auth_url,
|
|
55
|
+
organization_name, tenant_name = extract_org_tenant(
|
|
56
|
+
external_app_service._base_url
|
|
57
|
+
)
|
|
58
|
+
if not (organization_name and tenant_name):
|
|
59
|
+
self._console.warning(
|
|
60
|
+
"--base-url should include both organization and tenant, "
|
|
61
|
+
"e.g., 'https://cloud.uipath.com/{organization}/{tenant}'."
|
|
102
62
|
)
|
|
103
|
-
server = HTTPServer(port=auth_config["port"])
|
|
104
|
-
token_data = asyncio.run(server.start(state, code_verifier, self._domain))
|
|
105
63
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
64
|
+
env_vars = {
|
|
65
|
+
"UIPATH_ACCESS_TOKEN": token_data.access_token,
|
|
66
|
+
"UIPATH_URL": external_app_service._base_url,
|
|
67
|
+
"UIPATH_ORGANIZATION_ID": get_parsed_token_data(token_data).get("prt_id"),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if tenant_name:
|
|
71
|
+
self._tenant = tenant_name
|
|
72
|
+
with PortalService(
|
|
73
|
+
self._domain, access_token=token_data.access_token
|
|
74
|
+
) as portal_service:
|
|
75
|
+
tenant_info = portal_service.resolve_tenant_info(self._tenant)
|
|
76
|
+
env_vars["UIPATH_TENANT_ID"] = tenant_info["tenant_id"]
|
|
77
|
+
else:
|
|
78
|
+
self._console.warning("Could not extract tenant from --base-url.")
|
|
79
|
+
update_env_file(env_vars)
|
|
80
|
+
|
|
81
|
+
def _authenticate_authorization_code(self) -> None:
|
|
82
|
+
with PortalService(self._domain) as portal_service:
|
|
83
|
+
if not self._force and self._can_reuse_existing_token(portal_service):
|
|
84
|
+
return
|
|
110
85
|
|
|
86
|
+
token_data = self._perform_oauth_flow()
|
|
111
87
|
portal_service.update_token_data(token_data)
|
|
112
|
-
update_auth_file(token_data)
|
|
113
|
-
access_token = token_data["access_token"]
|
|
114
|
-
update_env_file({"UIPATH_ACCESS_TOKEN": access_token})
|
|
115
88
|
|
|
116
|
-
|
|
89
|
+
tenant_info = portal_service.resolve_tenant_info(self._tenant)
|
|
90
|
+
uipath_url = portal_service.build_tenant_url()
|
|
117
91
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
92
|
+
update_env_file(
|
|
93
|
+
{
|
|
94
|
+
"UIPATH_ACCESS_TOKEN": token_data.access_token,
|
|
95
|
+
"UIPATH_URL": uipath_url,
|
|
96
|
+
"UIPATH_TENANT_ID": tenant_info["tenant_id"],
|
|
97
|
+
"UIPATH_ORGANIZATION_ID": tenant_info["organization_id"],
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
portal_service.enable_studio_web(uipath_url)
|
|
103
|
+
except Exception:
|
|
104
|
+
self._console.error(
|
|
105
|
+
"Could not prepare the environment. Please try again."
|
|
121
106
|
)
|
|
122
|
-
else:
|
|
123
|
-
base_url = select_tenant(self._domain, tenants_and_organizations)
|
|
124
107
|
|
|
108
|
+
def _can_reuse_existing_token(self, portal_service: PortalService) -> bool:
|
|
109
|
+
if (
|
|
110
|
+
os.getenv("UIPATH_URL")
|
|
111
|
+
and os.getenv("UIPATH_TENANT_ID")
|
|
112
|
+
and os.getenv("UIPATH_ORGANIZATION_ID")
|
|
113
|
+
):
|
|
125
114
|
try:
|
|
126
|
-
portal_service.
|
|
115
|
+
portal_service.ensure_valid_token()
|
|
116
|
+
return True
|
|
127
117
|
except Exception:
|
|
128
118
|
self._console.error(
|
|
129
|
-
"
|
|
119
|
+
"Authentication token is invalid. Please reauthenticate using the '--force' flag."
|
|
130
120
|
)
|
|
121
|
+
return False
|
|
131
122
|
|
|
132
|
-
def
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
try:
|
|
136
|
-
s.bind(("localhost", target_port))
|
|
137
|
-
s.close()
|
|
138
|
-
return False
|
|
139
|
-
except error:
|
|
140
|
-
return True
|
|
123
|
+
def _perform_oauth_flow(self) -> TokenData:
|
|
124
|
+
auth_url, code_verifier, state = OidcUtils.get_auth_url(self._domain)
|
|
125
|
+
self._open_browser(auth_url)
|
|
141
126
|
|
|
142
127
|
auth_config = OidcUtils.get_auth_config()
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
auth_config["port"] = port
|
|
161
|
-
with open(
|
|
162
|
-
os.path.join(os.path.dirname(__file__), "..", "auth_config.json"), "w"
|
|
163
|
-
) as f:
|
|
164
|
-
json.dump(auth_config, f)
|
|
128
|
+
server = HTTPServer(port=auth_config["port"])
|
|
129
|
+
token_data = asyncio.run(server.start(state, code_verifier, self._domain))
|
|
130
|
+
|
|
131
|
+
if not token_data:
|
|
132
|
+
self._console.error(
|
|
133
|
+
"Authentication failed. Please try again.",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return TokenData.model_validate(token_data)
|
|
137
|
+
|
|
138
|
+
def _open_browser(self, url: str) -> None:
|
|
139
|
+
# Try to open browser. Always print the fallback link.
|
|
140
|
+
webbrowser.open(url, new=1)
|
|
141
|
+
self._console.link(
|
|
142
|
+
"If a browser window did not open, please open the following URL in your browser:",
|
|
143
|
+
url,
|
|
144
|
+
)
|
uipath/_cli/_auth/_models.py
CHANGED
|
@@ -10,17 +10,6 @@ class AuthConfig(TypedDict):
|
|
|
10
10
|
scope: str
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class TokenData(TypedDict):
|
|
14
|
-
"""TypedDict for token data structure."""
|
|
15
|
-
|
|
16
|
-
access_token: str
|
|
17
|
-
refresh_token: str
|
|
18
|
-
expires_in: int
|
|
19
|
-
token_type: str
|
|
20
|
-
scope: str
|
|
21
|
-
id_token: str
|
|
22
|
-
|
|
23
|
-
|
|
24
13
|
class AccessTokenData(TypedDict):
|
|
25
14
|
"""TypedDict for access token data structure."""
|
|
26
15
|
|
uipath/_cli/_auth/_oidc_utils.py
CHANGED
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
from urllib.parse import urlencode
|
|
6
6
|
|
|
7
|
+
from .._utils._console import ConsoleLogger
|
|
7
8
|
from ._models import AuthConfig
|
|
8
9
|
from ._url_utils import build_service_url
|
|
9
10
|
|
|
@@ -25,6 +26,22 @@ def get_state_param() -> str:
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class OidcUtils:
|
|
29
|
+
_console = ConsoleLogger()
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def _find_free_port(cls, candidates: list[int]):
|
|
33
|
+
from socket import AF_INET, SOCK_STREAM, error, socket
|
|
34
|
+
|
|
35
|
+
def is_free(port: int) -> bool:
|
|
36
|
+
with socket(AF_INET, SOCK_STREAM) as s:
|
|
37
|
+
try:
|
|
38
|
+
s.bind(("localhost", port))
|
|
39
|
+
return True
|
|
40
|
+
except error:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
return next((p for p in candidates if is_free(p)), None)
|
|
44
|
+
|
|
28
45
|
@classmethod
|
|
29
46
|
def get_auth_config(cls) -> AuthConfig:
|
|
30
47
|
with open(
|
|
@@ -32,7 +49,19 @@ class OidcUtils:
|
|
|
32
49
|
) as f:
|
|
33
50
|
auth_config = json.load(f)
|
|
34
51
|
|
|
35
|
-
|
|
52
|
+
candidates = [
|
|
53
|
+
int(auth_config.get("port", 8104)),
|
|
54
|
+
int(auth_config.get("portOptionOne", 8104)),
|
|
55
|
+
int(auth_config.get("portOptionTwo", 8055)),
|
|
56
|
+
int(auth_config.get("portOptionThree", 42042)),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
port = cls._find_free_port(candidates)
|
|
60
|
+
if port is None:
|
|
61
|
+
ports_str = ", ".join(str(p) for p in candidates)
|
|
62
|
+
cls._console.error(
|
|
63
|
+
f"All configured ports ({ports_str}) are in use. Please close applications using these ports or configure different ports."
|
|
64
|
+
)
|
|
36
65
|
|
|
37
66
|
redirect_uri = auth_config["redirect_uri"].replace(
|
|
38
67
|
"__PY_REPLACE_PORT__", str(port)
|
|
@@ -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)
|
uipath/_cli/_utils/_common.py
CHANGED
|
@@ -12,19 +12,19 @@ from ..spinner import Spinner
|
|
|
12
12
|
def environment_options(function):
|
|
13
13
|
function = click.option(
|
|
14
14
|
"--alpha",
|
|
15
|
-
"
|
|
15
|
+
"environment",
|
|
16
16
|
flag_value="alpha",
|
|
17
17
|
help="Use alpha environment",
|
|
18
18
|
)(function)
|
|
19
19
|
function = click.option(
|
|
20
20
|
"--staging",
|
|
21
|
-
"
|
|
21
|
+
"environment",
|
|
22
22
|
flag_value="staging",
|
|
23
23
|
help="Use staging environment",
|
|
24
24
|
)(function)
|
|
25
25
|
function = click.option(
|
|
26
26
|
"--cloud",
|
|
27
|
-
"
|
|
27
|
+
"environment",
|
|
28
28
|
flag_value="cloud",
|
|
29
29
|
default=True,
|
|
30
30
|
help="Use production environment",
|
uipath/_cli/cli_auth.py
CHANGED
|
@@ -48,7 +48,7 @@ console = ConsoleLogger()
|
|
|
48
48
|
)
|
|
49
49
|
@track
|
|
50
50
|
def auth(
|
|
51
|
-
|
|
51
|
+
environment: str,
|
|
52
52
|
force: bool = False,
|
|
53
53
|
client_id: Optional[str] = None,
|
|
54
54
|
client_secret: Optional[str] = None,
|
|
@@ -70,7 +70,7 @@ def auth(
|
|
|
70
70
|
- Set UIPATH_DISABLE_SSL_VERIFY to disable SSL verification (not recommended)
|
|
71
71
|
"""
|
|
72
72
|
auth_service = AuthService(
|
|
73
|
-
|
|
73
|
+
environment=environment,
|
|
74
74
|
force=force,
|
|
75
75
|
client_id=client_id,
|
|
76
76
|
client_secret=client_secret,
|
uipath/_services/__init__.py
CHANGED
|
@@ -7,6 +7,7 @@ from .connections_service import ConnectionsService
|
|
|
7
7
|
from .context_grounding_service import ContextGroundingService
|
|
8
8
|
from .documents_service import DocumentsService
|
|
9
9
|
from .entities_service import EntitiesService
|
|
10
|
+
from .external_application_service import ExternalApplicationService
|
|
10
11
|
from .folder_service import FolderService
|
|
11
12
|
from .jobs_service import JobsService
|
|
12
13
|
from .llm_gateway_service import UiPathLlmChatService, UiPathOpenAIService
|
|
@@ -29,4 +30,5 @@ __all__ = [
|
|
|
29
30
|
"UiPathLlmChatService",
|
|
30
31
|
"FolderService",
|
|
31
32
|
"EntitiesService",
|
|
33
|
+
"ExternalApplicationService",
|
|
32
34
|
]
|
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
from
|
|
1
|
+
from os import environ as env
|
|
2
|
+
from typing import Optional
|
|
2
3
|
from urllib.parse import urlparse
|
|
3
4
|
|
|
4
5
|
import httpx
|
|
6
|
+
from httpx import HTTPStatusError, Request
|
|
5
7
|
|
|
6
|
-
from
|
|
7
|
-
from .._utils.
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
8
|
+
from .._utils._ssl_context import get_httpx_client_kwargs
|
|
9
|
+
from .._utils.constants import ENV_BASE_URL
|
|
10
|
+
from ..models.auth import TokenData
|
|
11
|
+
from ..models.exceptions import EnrichedException
|
|
10
12
|
|
|
11
|
-
console = ConsoleLogger()
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
class ClientCredentialsService:
|
|
14
|
+
class ExternalApplicationService:
|
|
15
15
|
"""Service for client credentials authentication flow."""
|
|
16
16
|
|
|
17
|
-
def __init__(self, base_url: str):
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
def __init__(self, base_url: Optional[str]):
|
|
18
|
+
if not (resolved_base_url := (base_url or env.get(ENV_BASE_URL))):
|
|
19
|
+
raise ValueError(
|
|
20
|
+
"Base URL must be set either via constructor or the BASE_URL environment variable."
|
|
21
|
+
)
|
|
22
|
+
self._base_url = resolved_base_url
|
|
23
|
+
self._domain = self._extract_environment_from_base_url(self._base_url)
|
|
20
24
|
|
|
21
25
|
def get_token_url(self) -> str:
|
|
22
26
|
"""Get the token URL for the specified domain."""
|
|
@@ -40,7 +44,7 @@ class ClientCredentialsService:
|
|
|
40
44
|
"""
|
|
41
45
|
return hostname == domain or hostname.endswith(f".{domain}")
|
|
42
46
|
|
|
43
|
-
def
|
|
47
|
+
def _extract_environment_from_base_url(self, base_url: str) -> str:
|
|
44
48
|
"""Extract domain from base URL.
|
|
45
49
|
|
|
46
50
|
Args:
|
|
@@ -70,9 +74,9 @@ class ClientCredentialsService:
|
|
|
70
74
|
# Default to cloud if parsing fails
|
|
71
75
|
return "cloud"
|
|
72
76
|
|
|
73
|
-
def
|
|
77
|
+
def get_token_data(
|
|
74
78
|
self, client_id: str, client_secret: str, scope: Optional[str] = "OR.Execution"
|
|
75
|
-
) ->
|
|
79
|
+
) -> TokenData:
|
|
76
80
|
"""Authenticate using client credentials flow.
|
|
77
81
|
|
|
78
82
|
Args:
|
|
@@ -81,7 +85,7 @@ class ClientCredentialsService:
|
|
|
81
85
|
scope: The scope for the token (default: OR.Execution)
|
|
82
86
|
|
|
83
87
|
Returns:
|
|
84
|
-
Token data if successful
|
|
88
|
+
Token data if successful
|
|
85
89
|
"""
|
|
86
90
|
token_url = self.get_token_url()
|
|
87
91
|
|
|
@@ -97,49 +101,40 @@ class ClientCredentialsService:
|
|
|
97
101
|
response = client.post(token_url, data=data)
|
|
98
102
|
match response.status_code:
|
|
99
103
|
case 200:
|
|
100
|
-
|
|
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)
|
|
104
|
+
return TokenData.model_validate(response.json())
|
|
113
105
|
case 400:
|
|
114
|
-
|
|
115
|
-
|
|
106
|
+
raise EnrichedException(
|
|
107
|
+
HTTPStatusError(
|
|
108
|
+
message="Invalid client credentials or request parameters.",
|
|
109
|
+
request=Request(
|
|
110
|
+
data=data, url=token_url, method="post"
|
|
111
|
+
),
|
|
112
|
+
response=response,
|
|
113
|
+
)
|
|
116
114
|
)
|
|
117
115
|
case 401:
|
|
118
|
-
|
|
116
|
+
raise EnrichedException(
|
|
117
|
+
HTTPStatusError(
|
|
118
|
+
message="Unauthorized: Invalid client credentials.",
|
|
119
|
+
request=Request(
|
|
120
|
+
data=data, url=token_url, method="post"
|
|
121
|
+
),
|
|
122
|
+
response=response,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
119
125
|
case _:
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
raise EnrichedException(
|
|
127
|
+
HTTPStatusError(
|
|
128
|
+
message=f"Authentication failed with unexpected status: {response.status_code}",
|
|
129
|
+
request=Request(
|
|
130
|
+
data=data, url=token_url, method="post"
|
|
131
|
+
),
|
|
132
|
+
response=response,
|
|
133
|
+
)
|
|
122
134
|
)
|
|
123
|
-
|
|
135
|
+
except EnrichedException:
|
|
136
|
+
raise
|
|
124
137
|
except httpx.RequestError as e:
|
|
125
|
-
|
|
138
|
+
raise Exception(f"Network error during authentication: {e}") from e
|
|
126
139
|
except Exception as e:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def _setup_environment(self, token_data: TokenData):
|
|
130
|
-
"""Setup environment variables for client credentials authentication.
|
|
131
|
-
|
|
132
|
-
Args:
|
|
133
|
-
token_data: The token data from authentication
|
|
134
|
-
base_url: The base URL for the UiPath instance
|
|
135
|
-
"""
|
|
136
|
-
parsed_access_token = parse_access_token(token_data["access_token"])
|
|
137
|
-
|
|
138
|
-
env_vars = {
|
|
139
|
-
"UIPATH_ACCESS_TOKEN": token_data["access_token"],
|
|
140
|
-
"UIPATH_URL": self._base_url,
|
|
141
|
-
"UIPATH_ORGANIZATION_ID": parsed_access_token.get("prt_id", ""),
|
|
142
|
-
"UIPATH_TENANT_ID": "",
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
update_env_file(env_vars)
|
|
140
|
+
raise Exception(f"Unexpected error during authentication: {e}") from e
|
uipath/_uipath.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from os import environ as env
|
|
2
1
|
from typing import Optional
|
|
3
2
|
|
|
4
3
|
from pydantic import ValidationError
|
|
@@ -23,11 +22,7 @@ from ._services import (
|
|
|
23
22
|
UiPathOpenAIService,
|
|
24
23
|
)
|
|
25
24
|
from ._utils import setup_logging
|
|
26
|
-
from ._utils.
|
|
27
|
-
ENV_BASE_URL,
|
|
28
|
-
ENV_UIPATH_ACCESS_TOKEN,
|
|
29
|
-
ENV_UNATTENDED_USER_ACCESS_TOKEN,
|
|
30
|
-
)
|
|
25
|
+
from ._utils._auth import resolve_config
|
|
31
26
|
from .models.errors import BaseUrlMissingError, SecretMissingError
|
|
32
27
|
|
|
33
28
|
|
|
@@ -37,19 +32,18 @@ class UiPath:
|
|
|
37
32
|
*,
|
|
38
33
|
base_url: Optional[str] = None,
|
|
39
34
|
secret: Optional[str] = None,
|
|
35
|
+
client_id: Optional[str] = None,
|
|
36
|
+
client_secret: Optional[str] = None,
|
|
37
|
+
scope: Optional[str] = None,
|
|
40
38
|
debug: bool = False,
|
|
41
39
|
) -> None:
|
|
42
|
-
base_url_value = base_url or env.get(ENV_BASE_URL)
|
|
43
|
-
secret_value = (
|
|
44
|
-
secret
|
|
45
|
-
or env.get(ENV_UNATTENDED_USER_ACCESS_TOKEN)
|
|
46
|
-
or env.get(ENV_UIPATH_ACCESS_TOKEN)
|
|
47
|
-
)
|
|
48
|
-
|
|
49
40
|
try:
|
|
41
|
+
base_url, secret = resolve_config(
|
|
42
|
+
base_url, secret, client_id, client_secret, scope
|
|
43
|
+
)
|
|
50
44
|
self._config = Config(
|
|
51
|
-
base_url=
|
|
52
|
-
secret=
|
|
45
|
+
base_url=base_url,
|
|
46
|
+
secret=secret,
|
|
53
47
|
)
|
|
54
48
|
except ValidationError as e:
|
|
55
49
|
for error in e.errors():
|
uipath/_utils/_auth.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
from os import environ as env
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .._services import ExternalApplicationService
|
|
8
|
+
from .constants import (
|
|
9
|
+
ENV_BASE_URL,
|
|
10
|
+
ENV_UIPATH_ACCESS_TOKEN,
|
|
11
|
+
ENV_UNATTENDED_USER_ACCESS_TOKEN,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_access_token(access_token: str):
|
|
16
|
+
token_parts = access_token.split(".")
|
|
17
|
+
if len(token_parts) < 2:
|
|
18
|
+
raise Exception("Invalid access token")
|
|
19
|
+
payload = base64.urlsafe_b64decode(
|
|
20
|
+
token_parts[1] + "=" * (-len(token_parts[1]) % 4)
|
|
21
|
+
)
|
|
22
|
+
return json.loads(payload)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def update_env_file(env_contents):
|
|
26
|
+
env_path = Path.cwd() / ".env"
|
|
27
|
+
if env_path.exists():
|
|
28
|
+
with open(env_path, "r") as f:
|
|
29
|
+
for line in f:
|
|
30
|
+
if "=" in line:
|
|
31
|
+
key, value = line.strip().split("=", 1)
|
|
32
|
+
if key not in env_contents:
|
|
33
|
+
env_contents[key] = value
|
|
34
|
+
lines = [f"{key}={value}\n" for key, value in env_contents.items()]
|
|
35
|
+
with open(env_path, "w") as f:
|
|
36
|
+
f.writelines(lines)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _has_valid_client_credentials(
|
|
40
|
+
client_id: Optional[str], client_secret: Optional[str]
|
|
41
|
+
) -> bool:
|
|
42
|
+
if bool(client_id) != bool(client_secret):
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"Both client_id and client_secret must be provided together for Client Credentials Authentication."
|
|
45
|
+
)
|
|
46
|
+
return bool(client_id and client_secret)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve_config(
|
|
50
|
+
base_url: Optional[str],
|
|
51
|
+
secret: Optional[str],
|
|
52
|
+
client_id: Optional[str],
|
|
53
|
+
client_secret: Optional[str],
|
|
54
|
+
scope: Optional[str],
|
|
55
|
+
):
|
|
56
|
+
if _has_valid_client_credentials(client_id, client_secret):
|
|
57
|
+
external_app_service = ExternalApplicationService(base_url)
|
|
58
|
+
token_data = external_app_service.get_token_data(
|
|
59
|
+
client_id, # type: ignore
|
|
60
|
+
client_secret, # type: ignore
|
|
61
|
+
scope,
|
|
62
|
+
)
|
|
63
|
+
parsed_access_token = parse_access_token(token_data.access_token)
|
|
64
|
+
|
|
65
|
+
env_vars = {
|
|
66
|
+
"UIPATH_ACCESS_TOKEN": token_data.access_token,
|
|
67
|
+
"UIPATH_URL": external_app_service._base_url,
|
|
68
|
+
"UIPATH_ORGANIZATION_ID": parsed_access_token.get("prt_id", ""),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
update_env_file(env_vars)
|
|
72
|
+
return external_app_service._base_url, token_data.access_token
|
|
73
|
+
|
|
74
|
+
base_url_value = base_url or env.get(ENV_BASE_URL)
|
|
75
|
+
|
|
76
|
+
secret_value = (
|
|
77
|
+
secret
|
|
78
|
+
or env.get(ENV_UNATTENDED_USER_ACCESS_TOKEN)
|
|
79
|
+
or env.get(ENV_UIPATH_ACCESS_TOKEN)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return base_url_value, secret_value
|
uipath/models/auth.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TokenData(BaseModel):
|
|
7
|
+
"""Pydantic model for token data structure."""
|
|
8
|
+
|
|
9
|
+
access_token: str
|
|
10
|
+
refresh_token: Optional[str] = None
|
|
11
|
+
expires_in: Optional[int] = None
|
|
12
|
+
token_type: Optional[str] = None
|
|
13
|
+
scope: Optional[str] = None
|
|
14
|
+
id_token: Optional[str] = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uipath
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.81
|
|
4
4
|
Summary: Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools.
|
|
5
5
|
Project-URL: Homepage, https://uipath.com
|
|
6
6
|
Project-URL: Repository, https://github.com/UiPath/uipath-python
|
|
@@ -2,11 +2,11 @@ uipath/__init__.py,sha256=IaeKItOOQXMa95avueJ3dAq-XcRHyZVNjcCGwlSB000,634
|
|
|
2
2
|
uipath/_config.py,sha256=pi3qxPzDTxMEstj_XkGOgKJqD6RTHHv7vYv8sS_-d5Q,92
|
|
3
3
|
uipath/_execution_context.py,sha256=Qo8VMUFgtiL-40KsZrvul5bGv1CRERle_fCw1ORCggY,2374
|
|
4
4
|
uipath/_folder_context.py,sha256=D-bgxdwpwJP4b_QdVKcPODYh15kMDrOar2xNonmMSm4,1861
|
|
5
|
-
uipath/_uipath.py,sha256=
|
|
5
|
+
uipath/_uipath.py,sha256=mhtdu36-A-2WhPGNFFrTmQWbE07Bn4IV_Vrqgf6kuVM,4900
|
|
6
6
|
uipath/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
uipath/_cli/README.md,sha256=GLtCfbeIKZKNnGTCsfSVqRQ27V1btT1i2bSAyW_xZl4,474
|
|
8
8
|
uipath/_cli/__init__.py,sha256=2RUgXYd8uJaYjA67xWb0w4IZuBmZoY8G1ccNmEQk9oM,2343
|
|
9
|
-
uipath/_cli/cli_auth.py,sha256=
|
|
9
|
+
uipath/_cli/cli_auth.py,sha256=CzetSRqSUvMs02PtI4w5Vi_0fv_ETA307bB2vXalWzY,2628
|
|
10
10
|
uipath/_cli/cli_deploy.py,sha256=KPCmQ0c_NYD5JofSDao5r6QYxHshVCRxlWDVnQvlp5w,645
|
|
11
11
|
uipath/_cli/cli_dev.py,sha256=nEfpjw1PZ72O6jmufYWVrueVwihFxDPOeJakdvNHdOA,2146
|
|
12
12
|
uipath/_cli/cli_eval.py,sha256=oOMywGSUrHDQ1W_54ccbekzCeduPf-KHRyu_r0Dezd0,5444
|
|
@@ -21,13 +21,12 @@ uipath/_cli/cli_run.py,sha256=1FKv20EjxrrP1I5rNSnL_HzbWtOAIMjB3M--4RPA_Yo,3709
|
|
|
21
21
|
uipath/_cli/middlewares.py,sha256=0D9a-wphyetnH9T97F08o7-1OKWF1lMweFHHAR0xiOw,4979
|
|
22
22
|
uipath/_cli/spinner.py,sha256=bS-U_HA5yne11ejUERu7CQoXmWdabUD2bm62EfEdV8M,1107
|
|
23
23
|
uipath/_cli/_auth/_auth_server.py,sha256=22km0F1NFNXgyLbvtAx3ssiQlVGHroLdtDCWEqiCiMg,7106
|
|
24
|
-
uipath/_cli/_auth/_auth_service.py,sha256=
|
|
25
|
-
uipath/_cli/_auth/
|
|
26
|
-
uipath/_cli/_auth/
|
|
27
|
-
uipath/_cli/_auth/
|
|
28
|
-
uipath/_cli/_auth/
|
|
29
|
-
uipath/_cli/_auth/
|
|
30
|
-
uipath/_cli/_auth/_utils.py,sha256=9nb76xe5XmDZ0TAncp-_1SKqL6FdwRi9eS3C2noN1lY,1591
|
|
24
|
+
uipath/_cli/_auth/_auth_service.py,sha256=7UykDSZWhe8B152rnoS1wF0XkAkhAipfDwEtT2cW7rk,5245
|
|
25
|
+
uipath/_cli/_auth/_models.py,sha256=kWhqd5FqBo_oi3DW2x1IaJHsEloXq0I24f-pX5zb_O4,753
|
|
26
|
+
uipath/_cli/_auth/_oidc_utils.py,sha256=SLiOOAEdVwmPKRxIhFkARnNbrf01tvJxBAxa6MF9HHY,3458
|
|
27
|
+
uipath/_cli/_auth/_portal_service.py,sha256=sq3Ls7v8Q1gQWbYlE37_p82gLYPhC_cXJRTuIxMsCPU,7997
|
|
28
|
+
uipath/_cli/_auth/_url_utils.py,sha256=MuMYesMQDgetLBzkwd19dPxw3fctys7EVpByDUQMyLE,2844
|
|
29
|
+
uipath/_cli/_auth/_utils.py,sha256=To4Ara_UF4g7nzUfKqFA11lTjhQWIZWNm4xwa5nNKmU,896
|
|
31
30
|
uipath/_cli/_auth/auth_config.json,sha256=o8J5BBFwiEtjZLHpJ_64lvnTeYeRIHaJ-Bhg0QvcUX8,521
|
|
32
31
|
uipath/_cli/_auth/index.html,sha256=uGK0CDTP8Rys_p4O_Pbd2x4tz0frKNVcumjrXnal5Nc,22814
|
|
33
32
|
uipath/_cli/_auth/localhost.crt,sha256=oGl9oLLOiouHubAt39B4zEfylFvKEtbtr_43SIliXJc,1226
|
|
@@ -73,7 +72,7 @@ uipath/_cli/_templates/.rels.template,sha256=-fTcw7OA1AcymHr0LzBqbMAAtzZTRXLTNa_
|
|
|
73
72
|
uipath/_cli/_templates/[Content_Types].xml.template,sha256=bYsKDz31PkIF9QksjgAY_bqm57YC8U_owsZeNZAiBxQ,584
|
|
74
73
|
uipath/_cli/_templates/main.py.template,sha256=QB62qX5HKDbW4lFskxj7h9uuxBITnTWqu_DE6asCwcU,476
|
|
75
74
|
uipath/_cli/_templates/package.nuspec.template,sha256=YZyLc-u_EsmIoKf42JsLQ55OGeFmb8VkIU2VF7DFbtw,359
|
|
76
|
-
uipath/_cli/_utils/_common.py,sha256=
|
|
75
|
+
uipath/_cli/_utils/_common.py,sha256=bYgqIdlxxlB_4s8BlZsfGbSdbfm93MFtOj5ydptbkPI,3344
|
|
77
76
|
uipath/_cli/_utils/_console.py,sha256=scvnrrFoFX6CE451K-PXKV7UN0DUkInbOtDZ5jAdPP0,10070
|
|
78
77
|
uipath/_cli/_utils/_constants.py,sha256=rS8lQ5Nzull8ytajK6lBsz398qiCp1REoAwlHtyBwF0,1415
|
|
79
78
|
uipath/_cli/_utils/_debug.py,sha256=zamzIR4VgbdKADAE4gbmjxDsbgF7wvdr7C5Dqp744Oc,1739
|
|
@@ -90,7 +89,7 @@ uipath/_events/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
90
89
|
uipath/_events/_event_bus.py,sha256=4-VzstyX69cr7wT1EY7ywp-Ndyz2CyemD3Wk_-QmRpo,5496
|
|
91
90
|
uipath/_events/_events.py,sha256=EzDfzpVm-EIH27Onad5mo8Go6-WB3S6_-6zZQ7qV58w,1811
|
|
92
91
|
uipath/_resources/AGENTS.md,sha256=YWhWuX9XIbyVhVT3PnPc4Of3_q6bsNJcuzYu3N8f_Ug,25850
|
|
93
|
-
uipath/_services/__init__.py,sha256=
|
|
92
|
+
uipath/_services/__init__.py,sha256=_LNy4u--VlhVtTO66bULbCoBjyJBTuyh9jnzjWrv-h4,1140
|
|
94
93
|
uipath/_services/_base_service.py,sha256=x9-9jhPzn9Z16KRdFHhJNvV-FZHvTniMsDfxlS4Cutk,5782
|
|
95
94
|
uipath/_services/actions_service.py,sha256=2RPMR-hFMsOlqEyjIf3aF7-lrf57jdrSD0pBjj0Kyko,16040
|
|
96
95
|
uipath/_services/api_client.py,sha256=kGm04ijk9AOEQd2BMxvQg-2QoB8dmyoDwFFDPyutAGw,1966
|
|
@@ -101,12 +100,14 @@ uipath/_services/connections_service.py,sha256=IqhKdRYwNZlRsDL2vY7gyl5nAiYaK1zvj
|
|
|
101
100
|
uipath/_services/context_grounding_service.py,sha256=Pjx-QQQEiSKD-hY6ityj3QUSALN3fIcKLLHr_NZ0d_g,37117
|
|
102
101
|
uipath/_services/documents_service.py,sha256=UnFS8EpOZ_Ng2TZk3OiJJ3iNANvFs7QxuoG_v-lQj6c,24815
|
|
103
102
|
uipath/_services/entities_service.py,sha256=QKCLE6wRgq3HZraF-M2mljy-8il4vsNHrQhUgkewVVk,14028
|
|
103
|
+
uipath/_services/external_application_service.py,sha256=gZhnGgLn7ZYUZzZF7AumB14QEPanVY-D_02FqEcAFtw,5478
|
|
104
104
|
uipath/_services/folder_service.py,sha256=9JqgjKhWD-G_KUnfUTP2BADxL6OK9QNZsBsWZHAULdE,2749
|
|
105
105
|
uipath/_services/jobs_service.py,sha256=tTZNsdZKN3uP7bWPQyBCpJeQxTfuOWbKYOR4L-_yJo4,32736
|
|
106
106
|
uipath/_services/llm_gateway_service.py,sha256=oFBKSYbZqujGHDuM3A72S3J7mHn9kWjNTgcE3U0c24Y,24411
|
|
107
107
|
uipath/_services/processes_service.py,sha256=O_uHgQ1rnwiV5quG0OQqabAnE6Rf6cWrMENYY2jKWt8,8585
|
|
108
108
|
uipath/_services/queues_service.py,sha256=VaG3dWL2QK6AJBOLoW2NQTpkPfZjsqsYPl9-kfXPFzA,13534
|
|
109
109
|
uipath/_utils/__init__.py,sha256=VdcpnENJIa0R6Y26NoxY64-wUVyvb4pKfTh1wXDQeMk,526
|
|
110
|
+
uipath/_utils/_auth.py,sha256=5R30ALuRrIatf_0Lk-AETJvt6ora7h0R7yeDdliW1Lg,2524
|
|
110
111
|
uipath/_utils/_endpoint.py,sha256=yYHwqbQuJIevpaTkdfYJS9CrtlFeEyfb5JQK5osTCog,2489
|
|
111
112
|
uipath/_utils/_infer_bindings.py,sha256=eCxfUjd37fOFZ6vOfKl2BhWVUt7iSSJ-VQ0-nDTBcaA,1836
|
|
112
113
|
uipath/_utils/_logs.py,sha256=adfX_0UAn3YBeKJ8DQDeZs94rJyHGQO00uDfkaTpNWQ,510
|
|
@@ -147,6 +148,7 @@ uipath/models/action_schema.py,sha256=tBn1qQ3NQLU5nwWlBIzIKIx3XK5pO_D1S51IjFlZ1F
|
|
|
147
148
|
uipath/models/actions.py,sha256=1vRsJ3JSmMdPkbiYAiHzY8K44vmW3VlMsmQUBAkSgrQ,3141
|
|
148
149
|
uipath/models/assets.py,sha256=7x3swJRnG_a4VgjdXKKwraJLT5TF0u4wHsl6coOjX0g,2762
|
|
149
150
|
uipath/models/attachment.py,sha256=lI6BxBY6DY5U6qZbxhkNu-usseA1zovYSTRtLq50ubI,1029
|
|
151
|
+
uipath/models/auth.py,sha256=-CEo5KZVtZZgbAMatN6B1vBmGp8lTTumR8sMthRmL8I,345
|
|
150
152
|
uipath/models/buckets.py,sha256=N3Lj_dVCv709-ywhOOdyCSvsuLn41eGuAfSiik6Q6F8,1285
|
|
151
153
|
uipath/models/connections.py,sha256=bTDg8xISSPmKB1GFNEEMD1OEZyBDFHfZVKqw4gab1pE,2524
|
|
152
154
|
uipath/models/context_grounding.py,sha256=3MaF2Fv2QYle8UUWvKGkCN5XGpx2T4a34fdbBqJ2fCs,1137
|
|
@@ -169,8 +171,8 @@ uipath/tracing/_traced.py,sha256=yBIY05PCCrYyx50EIHZnwJaKNdHPNx-YTR1sHQl0a98,199
|
|
|
169
171
|
uipath/tracing/_utils.py,sha256=qd7N56tg6VXQ9pREh61esBgUWLNA0ssKsE0QlwrRWFM,11974
|
|
170
172
|
uipath/utils/__init__.py,sha256=VD-KXFpF_oWexFg6zyiWMkxl2HM4hYJMIUDZ1UEtGx0,105
|
|
171
173
|
uipath/utils/_endpoints_manager.py,sha256=iRTl5Q0XAm_YgcnMcJOXtj-8052sr6jpWuPNz6CgT0Q,8408
|
|
172
|
-
uipath-2.1.
|
|
173
|
-
uipath-2.1.
|
|
174
|
-
uipath-2.1.
|
|
175
|
-
uipath-2.1.
|
|
176
|
-
uipath-2.1.
|
|
174
|
+
uipath-2.1.81.dist-info/METADATA,sha256=8-3eAQ0tiFCXMWOhGm8QTSvM9fATrjo6j-UnceXoMOE,6593
|
|
175
|
+
uipath-2.1.81.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
176
|
+
uipath-2.1.81.dist-info/entry_points.txt,sha256=9C2_29U6Oq1ExFu7usihR-dnfIVNSKc-0EFbh0rskB4,43
|
|
177
|
+
uipath-2.1.81.dist-info/licenses/LICENSE,sha256=-KBavWXepyDjimmzH5fVAsi-6jNVpIKFc2kZs0Ri4ng,1058
|
|
178
|
+
uipath-2.1.81.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|