uipath 2.0.0.dev2__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of uipath might be problematic. Click here for more details.
- uipath/__init__.py +24 -0
- uipath/_cli/README.md +11 -0
- uipath/_cli/__init__.py +54 -0
- uipath/_cli/_auth/_auth_server.py +165 -0
- uipath/_cli/_auth/_models.py +51 -0
- uipath/_cli/_auth/_oidc_utils.py +69 -0
- uipath/_cli/_auth/_portal_service.py +163 -0
- uipath/_cli/_auth/_utils.py +51 -0
- uipath/_cli/_auth/auth_config.json +6 -0
- uipath/_cli/_auth/index.html +167 -0
- uipath/_cli/_auth/localhost.crt +25 -0
- uipath/_cli/_auth/localhost.key +27 -0
- uipath/_cli/_runtime/_contracts.py +429 -0
- uipath/_cli/_runtime/_logging.py +193 -0
- uipath/_cli/_runtime/_runtime.py +264 -0
- uipath/_cli/_templates/.psmdcp.template +9 -0
- uipath/_cli/_templates/.rels.template +5 -0
- uipath/_cli/_templates/[Content_Types].xml.template +9 -0
- uipath/_cli/_templates/main.py.template +25 -0
- uipath/_cli/_templates/package.nuspec.template +10 -0
- uipath/_cli/_utils/_common.py +24 -0
- uipath/_cli/_utils/_input_args.py +126 -0
- uipath/_cli/_utils/_parse_ast.py +542 -0
- uipath/_cli/cli_auth.py +97 -0
- uipath/_cli/cli_deploy.py +13 -0
- uipath/_cli/cli_init.py +113 -0
- uipath/_cli/cli_new.py +76 -0
- uipath/_cli/cli_pack.py +337 -0
- uipath/_cli/cli_publish.py +113 -0
- uipath/_cli/cli_run.py +133 -0
- uipath/_cli/middlewares.py +113 -0
- uipath/_config.py +6 -0
- uipath/_execution_context.py +83 -0
- uipath/_folder_context.py +62 -0
- uipath/_models/__init__.py +37 -0
- uipath/_models/action_schema.py +26 -0
- uipath/_models/actions.py +64 -0
- uipath/_models/assets.py +48 -0
- uipath/_models/connections.py +51 -0
- uipath/_models/context_grounding.py +18 -0
- uipath/_models/context_grounding_index.py +60 -0
- uipath/_models/exceptions.py +6 -0
- uipath/_models/interrupt_models.py +28 -0
- uipath/_models/job.py +66 -0
- uipath/_models/llm_gateway.py +101 -0
- uipath/_models/processes.py +48 -0
- uipath/_models/queues.py +167 -0
- uipath/_services/__init__.py +26 -0
- uipath/_services/_base_service.py +250 -0
- uipath/_services/actions_service.py +271 -0
- uipath/_services/api_client.py +89 -0
- uipath/_services/assets_service.py +257 -0
- uipath/_services/buckets_service.py +268 -0
- uipath/_services/connections_service.py +185 -0
- uipath/_services/connections_service.pyi +50 -0
- uipath/_services/context_grounding_service.py +402 -0
- uipath/_services/folder_service.py +49 -0
- uipath/_services/jobs_service.py +265 -0
- uipath/_services/llm_gateway_service.py +311 -0
- uipath/_services/processes_service.py +168 -0
- uipath/_services/queues_service.py +314 -0
- uipath/_uipath.py +98 -0
- uipath/_utils/__init__.py +17 -0
- uipath/_utils/_endpoint.py +79 -0
- uipath/_utils/_infer_bindings.py +30 -0
- uipath/_utils/_logs.py +15 -0
- uipath/_utils/_request_override.py +18 -0
- uipath/_utils/_request_spec.py +23 -0
- uipath/_utils/_user_agent.py +16 -0
- uipath/_utils/constants.py +25 -0
- uipath/py.typed +0 -0
- {uipath-2.0.0.dev2.dist-info → uipath-2.0.1.dist-info}/METADATA +2 -3
- uipath-2.0.1.dist-info/RECORD +75 -0
- uipath-2.0.0.dev2.dist-info/RECORD +0 -4
- {uipath-2.0.0.dev2.dist-info → uipath-2.0.1.dist-info}/WHEEL +0 -0
- {uipath-2.0.0.dev2.dist-info → uipath-2.0.1.dist-info}/entry_points.txt +0 -0
uipath/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""UiPath SDK for Python.
|
|
2
|
+
|
|
3
|
+
This package provides a Python interface to interact with UiPath's automation platform.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
The main entry point is the UiPath class, which provides access to all SDK functionality.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
```python
|
|
10
|
+
# First set these environment variables:
|
|
11
|
+
# export UIPATH_URL="https://cloud.uipath.com/organization-name/default-tenant"
|
|
12
|
+
# export UIPATH_ACCESS_TOKEN="your_**_token"
|
|
13
|
+
# export UIPATH_FOLDER_PATH="your/folder/path"
|
|
14
|
+
|
|
15
|
+
from uipath import UiPath
|
|
16
|
+
sdk = UiPath()
|
|
17
|
+
# Invoke a process by name
|
|
18
|
+
sdk.processes.invoke("MyProcess")
|
|
19
|
+
```
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from ._uipath import UiPath
|
|
23
|
+
|
|
24
|
+
__all__ = ["UiPath"]
|
uipath/_cli/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# parse
|
|
2
|
+
|
|
3
|
+
`parse-ast.py` will extract names and types of resources to prepare for bindings. currently it's hardcoded to run on `dummy-main.py`
|
|
4
|
+
|
|
5
|
+
# init
|
|
6
|
+
|
|
7
|
+
asks for project name, type (process/agent) description and target directory where to init the project. it inits a script, a requirements.txt for the user to install, and a config.json file that we'll use at packing time.
|
|
8
|
+
|
|
9
|
+
# pack
|
|
10
|
+
|
|
11
|
+
asks for target directory of the package files and the version and will create a nupkg file
|
uipath/_cli/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from .cli_auth import auth as auth # type: ignore
|
|
7
|
+
from .cli_deploy import deploy as deploy # type: ignore
|
|
8
|
+
from .cli_init import init as init # type: ignore
|
|
9
|
+
from .cli_new import new as new # type: ignore
|
|
10
|
+
from .cli_pack import pack as pack # type: ignore
|
|
11
|
+
from .cli_publish import publish as publish # type: ignore
|
|
12
|
+
from .cli_run import run as run # type: ignore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group(invoke_without_command=True)
|
|
16
|
+
@click.version_option(
|
|
17
|
+
importlib.metadata.version("uipath"),
|
|
18
|
+
prog_name="uipath",
|
|
19
|
+
message="%(prog)s version %(version)s",
|
|
20
|
+
)
|
|
21
|
+
@click.option(
|
|
22
|
+
"-lv",
|
|
23
|
+
is_flag=True,
|
|
24
|
+
help="Display the current version of uipath-langchain.",
|
|
25
|
+
)
|
|
26
|
+
@click.option(
|
|
27
|
+
"-v",
|
|
28
|
+
is_flag=True,
|
|
29
|
+
help="Display the current version of uipath.",
|
|
30
|
+
)
|
|
31
|
+
def cli(lv: bool, v: bool) -> None:
|
|
32
|
+
if lv:
|
|
33
|
+
try:
|
|
34
|
+
version = importlib.metadata.version("uipath-langchain")
|
|
35
|
+
click.echo(f"uipath-langchain version {version}")
|
|
36
|
+
except importlib.metadata.PackageNotFoundError:
|
|
37
|
+
click.echo("uipath-langchain is not installed", err=True)
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
if v:
|
|
40
|
+
try:
|
|
41
|
+
version = importlib.metadata.version("uipath")
|
|
42
|
+
click.echo(f"uipath version {version}")
|
|
43
|
+
except importlib.metadata.PackageNotFoundError:
|
|
44
|
+
click.echo("uipath is not installed", err=True)
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
cli.add_command(new)
|
|
49
|
+
cli.add_command(init)
|
|
50
|
+
cli.add_command(pack)
|
|
51
|
+
cli.add_command(publish)
|
|
52
|
+
cli.add_command(run)
|
|
53
|
+
cli.add_command(deploy)
|
|
54
|
+
cli.add_command(auth)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import http.server
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import socketserver
|
|
5
|
+
import ssl
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
from ._oidc_utils import get_auth_config
|
|
11
|
+
|
|
12
|
+
load_dotenv()
|
|
13
|
+
|
|
14
|
+
# Server port
|
|
15
|
+
PORT = 6234
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Custom exception for token received
|
|
19
|
+
class TokenReceivedSignal(Exception):
|
|
20
|
+
"""Exception raised when a token is successfully received."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, token_data):
|
|
23
|
+
self.token_data = token_data
|
|
24
|
+
super().__init__("Token received successfully")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def make_request_handler_class(state, code_verifier, token_callback, domain):
|
|
28
|
+
class SimpleHTTPSRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|
29
|
+
"""Simple HTTPS request handler that serves static files."""
|
|
30
|
+
|
|
31
|
+
def log_message(self, format, *args) -> None:
|
|
32
|
+
# do nothing
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def do_POST(self):
|
|
36
|
+
"""Handle POST requests to /set_token."""
|
|
37
|
+
if self.path == "/set_token":
|
|
38
|
+
content_length = int(self.headers["Content-Length"])
|
|
39
|
+
post_data = self.rfile.read(content_length)
|
|
40
|
+
token_data = json.loads(post_data.decode("utf-8"))
|
|
41
|
+
print("Received authentication information")
|
|
42
|
+
|
|
43
|
+
self.send_response(200)
|
|
44
|
+
self.end_headers()
|
|
45
|
+
self.wfile.write(b"Token received")
|
|
46
|
+
|
|
47
|
+
time.sleep(1)
|
|
48
|
+
|
|
49
|
+
token_callback(token_data)
|
|
50
|
+
elif self.path == "/log":
|
|
51
|
+
content_length = int(self.headers["Content-Length"])
|
|
52
|
+
post_data = self.rfile.read(content_length)
|
|
53
|
+
logs = json.loads(post_data.decode("utf-8"))
|
|
54
|
+
# Write logs to .uipath/.error_log file
|
|
55
|
+
uipath_dir = os.path.join(os.getcwd(), ".uipath")
|
|
56
|
+
os.makedirs(uipath_dir, exist_ok=True)
|
|
57
|
+
error_log_path = os.path.join(uipath_dir, ".error_log")
|
|
58
|
+
|
|
59
|
+
with open(error_log_path, "a", encoding="utf-8") as f:
|
|
60
|
+
f.write(
|
|
61
|
+
f"\n--- Authentication Error Log {time.strftime('%Y-%m-%d %H:%M:%S')} ---\n"
|
|
62
|
+
)
|
|
63
|
+
json.dump(logs, f, indent=2)
|
|
64
|
+
f.write("\n")
|
|
65
|
+
print(logs)
|
|
66
|
+
print("Received log data")
|
|
67
|
+
self.send_response(200)
|
|
68
|
+
self.end_headers()
|
|
69
|
+
self.wfile.write(b"Log received")
|
|
70
|
+
else:
|
|
71
|
+
self.send_error(404, "Path not found")
|
|
72
|
+
|
|
73
|
+
def do_GET(self):
|
|
74
|
+
"""Handle GET requests by serving index.html."""
|
|
75
|
+
# Always serve index.html regardless of the path
|
|
76
|
+
try:
|
|
77
|
+
index_path = os.path.join(os.path.dirname(__file__), "index.html")
|
|
78
|
+
with open(index_path, "r") as f:
|
|
79
|
+
content = f.read()
|
|
80
|
+
|
|
81
|
+
# Get the redirect URI from auth config
|
|
82
|
+
auth_config = get_auth_config()
|
|
83
|
+
redirect_uri = auth_config["redirect_uri"]
|
|
84
|
+
|
|
85
|
+
content = content.replace("__PY_REPLACE_EXPECTED_STATE__", state)
|
|
86
|
+
content = content.replace("__PY_REPLACE_CODE_VERIFIER__", code_verifier)
|
|
87
|
+
content = content.replace("__PY_REPLACE_REDIRECT_URI__", redirect_uri)
|
|
88
|
+
content = content.replace(
|
|
89
|
+
"__PY_REPLACE_CLIENT_ID__", auth_config["client_id"]
|
|
90
|
+
)
|
|
91
|
+
content = content.replace("__PY_REPLACE_DOMAIN__", domain)
|
|
92
|
+
|
|
93
|
+
self.send_response(200)
|
|
94
|
+
self.send_header("Content-Type", "text/html")
|
|
95
|
+
self.send_header("Content-Length", str(len(content)))
|
|
96
|
+
self.end_headers()
|
|
97
|
+
self.wfile.write(content.encode("utf-8"))
|
|
98
|
+
except FileNotFoundError:
|
|
99
|
+
self.send_error(404, "File not found")
|
|
100
|
+
|
|
101
|
+
def end_headers(self):
|
|
102
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
103
|
+
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
104
|
+
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
|
105
|
+
super().end_headers()
|
|
106
|
+
|
|
107
|
+
def do_OPTIONS(self):
|
|
108
|
+
self.send_response(200)
|
|
109
|
+
self.end_headers()
|
|
110
|
+
|
|
111
|
+
return SimpleHTTPSRequestHandler
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class HTTPSServer:
|
|
115
|
+
def __init__(self, port=6234, cert_file="localhost.crt", key_file="localhost.key"):
|
|
116
|
+
"""Initialize HTTPS server with configurable parameters."""
|
|
117
|
+
self.current_path = os.path.dirname(os.path.abspath(__file__))
|
|
118
|
+
self.port = port
|
|
119
|
+
self.cert_file = os.path.join(self.current_path, "localhost.crt")
|
|
120
|
+
self.key_file = os.path.join(self.current_path, "localhost.key")
|
|
121
|
+
self.httpd = None
|
|
122
|
+
self.token_data = None
|
|
123
|
+
self.should_shutdown = False
|
|
124
|
+
|
|
125
|
+
def token_received_callback(self, token_data):
|
|
126
|
+
"""Callback for when a token is received."""
|
|
127
|
+
self.token_data = token_data
|
|
128
|
+
self.should_shutdown = True
|
|
129
|
+
|
|
130
|
+
def create_server(self, state, code_verifier, domain):
|
|
131
|
+
"""Create and configure the HTTPS server."""
|
|
132
|
+
# Create SSL context
|
|
133
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
134
|
+
context.load_cert_chain(self.cert_file, self.key_file)
|
|
135
|
+
|
|
136
|
+
# Create server
|
|
137
|
+
handler = make_request_handler_class(
|
|
138
|
+
state, code_verifier, self.token_received_callback, domain
|
|
139
|
+
)
|
|
140
|
+
self.httpd = socketserver.TCPServer(("", self.port), handler)
|
|
141
|
+
self.httpd.socket = context.wrap_socket(self.httpd.socket, server_side=True)
|
|
142
|
+
|
|
143
|
+
return self.httpd
|
|
144
|
+
|
|
145
|
+
def start(self, state, code_verifier, domain):
|
|
146
|
+
"""Start the server."""
|
|
147
|
+
if not self.httpd:
|
|
148
|
+
self.create_server(state, code_verifier, domain)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
if self.httpd:
|
|
152
|
+
while not self.should_shutdown:
|
|
153
|
+
self.httpd.handle_request()
|
|
154
|
+
except KeyboardInterrupt:
|
|
155
|
+
print("Process interrupted by user")
|
|
156
|
+
finally:
|
|
157
|
+
self.stop()
|
|
158
|
+
|
|
159
|
+
return self.token_data if self.token_data else {}
|
|
160
|
+
|
|
161
|
+
def stop(self):
|
|
162
|
+
"""Stop the server gracefully."""
|
|
163
|
+
if self.httpd:
|
|
164
|
+
self.httpd.server_close()
|
|
165
|
+
self.httpd = None
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing import TypedDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AuthConfig(TypedDict):
|
|
5
|
+
"""TypedDict for auth_config.json structure."""
|
|
6
|
+
|
|
7
|
+
client_id: str
|
|
8
|
+
port: int
|
|
9
|
+
redirect_uri: str
|
|
10
|
+
scope: str
|
|
11
|
+
|
|
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
|
+
class AccessTokenData(TypedDict):
|
|
25
|
+
"""TypedDict for access token data structure."""
|
|
26
|
+
|
|
27
|
+
sub: str
|
|
28
|
+
prt_id: str
|
|
29
|
+
client_id: str
|
|
30
|
+
exp: float
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TenantInfo(TypedDict):
|
|
34
|
+
"""TypedDict for tenant info structure."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
id: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OrganizationInfo(TypedDict):
|
|
41
|
+
"""TypedDict for organization info structure."""
|
|
42
|
+
|
|
43
|
+
id: str
|
|
44
|
+
name: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TenantsAndOrganizationInfoResponse(TypedDict):
|
|
48
|
+
"""TypedDict for tenants and organization info response structure."""
|
|
49
|
+
|
|
50
|
+
tenants: list[TenantInfo]
|
|
51
|
+
organization: OrganizationInfo
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from urllib.parse import urlencode
|
|
6
|
+
|
|
7
|
+
from ._models import AuthConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generate_code_verifier_and_challenge():
|
|
11
|
+
"""Generate PKCE code verifier and challenge."""
|
|
12
|
+
code_verifier = base64.urlsafe_b64encode(os.urandom(32)).decode("utf-8").rstrip("=")
|
|
13
|
+
|
|
14
|
+
code_challenge_bytes = hashlib.sha256(code_verifier.encode("utf-8")).digest()
|
|
15
|
+
code_challenge = (
|
|
16
|
+
base64.urlsafe_b64encode(code_challenge_bytes).decode("utf-8").rstrip("=")
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return code_verifier, code_challenge
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_state_param() -> str:
|
|
23
|
+
return base64.urlsafe_b64encode(os.urandom(32)).decode("utf-8").rstrip("=")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_auth_config() -> AuthConfig:
|
|
27
|
+
auth_config = {}
|
|
28
|
+
with open(os.path.join(os.path.dirname(__file__), "auth_config.json"), "r") as f:
|
|
29
|
+
auth_config = json.load(f)
|
|
30
|
+
|
|
31
|
+
port = auth_config.get("port", 8104)
|
|
32
|
+
|
|
33
|
+
redirect_uri = auth_config["redirect_uri"].replace("__PY_REPLACE_PORT__", str(port))
|
|
34
|
+
|
|
35
|
+
return AuthConfig(
|
|
36
|
+
client_id=auth_config["client_id"],
|
|
37
|
+
redirect_uri=redirect_uri,
|
|
38
|
+
scope=auth_config["scope"],
|
|
39
|
+
port=port,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_auth_url(domain: str) -> tuple[str, str, str]:
|
|
44
|
+
"""Get the authorization URL for OAuth2 PKCE flow.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
domain (str): The UiPath domain to authenticate against (e.g. 'alpha', 'cloud')
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
tuple[str, str]: A tuple containing:
|
|
51
|
+
- The authorization URL with query parameters
|
|
52
|
+
- The code verifier for PKCE flow
|
|
53
|
+
"""
|
|
54
|
+
code_verifier, code_challenge = generate_code_verifier_and_challenge()
|
|
55
|
+
auth_config = get_auth_config()
|
|
56
|
+
state = get_state_param()
|
|
57
|
+
query_params = {
|
|
58
|
+
"client_id": auth_config["client_id"],
|
|
59
|
+
"redirect_uri": auth_config["redirect_uri"],
|
|
60
|
+
"response_type": "code",
|
|
61
|
+
"scope": auth_config["scope"],
|
|
62
|
+
"state": state,
|
|
63
|
+
"code_challenge": code_challenge,
|
|
64
|
+
"code_challenge_method": "S256",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
query_string = urlencode(query_params)
|
|
68
|
+
url = f"https://{domain}.uipath.com/identity_/connect/authorize?{query_string}"
|
|
69
|
+
return url, code_verifier, state
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from ._models import TenantsAndOrganizationInfoResponse, TokenData
|
|
9
|
+
from ._oidc_utils import get_auth_config
|
|
10
|
+
from ._utils import (
|
|
11
|
+
get_auth_data,
|
|
12
|
+
get_parsed_token_data,
|
|
13
|
+
update_auth_file,
|
|
14
|
+
update_env_file,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PortalService:
|
|
19
|
+
"""Service for interacting with the UiPath Portal API."""
|
|
20
|
+
|
|
21
|
+
access_token: Optional[str] = None
|
|
22
|
+
prt_id: Optional[str] = None
|
|
23
|
+
domain: Optional[str] = None
|
|
24
|
+
selected_tenant: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
_tenants_and_organizations: Optional[TenantsAndOrganizationInfoResponse] = None
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
domain: str,
|
|
31
|
+
access_token: Optional[str] = None,
|
|
32
|
+
prt_id: Optional[str] = None,
|
|
33
|
+
):
|
|
34
|
+
self.domain = domain
|
|
35
|
+
self.access_token = access_token
|
|
36
|
+
self.prt_id = prt_id
|
|
37
|
+
|
|
38
|
+
def update_token_data(self, token_data: TokenData):
|
|
39
|
+
self.access_token = token_data["access_token"]
|
|
40
|
+
self.prt_id = get_parsed_token_data(token_data).get("prt_id")
|
|
41
|
+
|
|
42
|
+
def get_tenants_and_organizations(self) -> TenantsAndOrganizationInfoResponse:
|
|
43
|
+
url = f"https://{self.domain}.uipath.com/{self.prt_id}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo"
|
|
44
|
+
response = requests.get(
|
|
45
|
+
url, headers={"Authorization": f"Bearer {self.access_token}"}
|
|
46
|
+
)
|
|
47
|
+
if response.ok:
|
|
48
|
+
result = response.json()
|
|
49
|
+
self._tenants_and_organizations = result
|
|
50
|
+
return result
|
|
51
|
+
elif response.status_code == 401:
|
|
52
|
+
raise Exception("Unauthorized")
|
|
53
|
+
else:
|
|
54
|
+
raise Exception(
|
|
55
|
+
f"Failed to get tenants and organizations: {response.status_code} {response.text}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def get_uipath_orchestrator_url(self) -> str:
|
|
59
|
+
if self._tenants_and_organizations is None:
|
|
60
|
+
self._tenants_and_organizations = self.get_tenants_and_organizations()
|
|
61
|
+
organization = self._tenants_and_organizations.get("organization")
|
|
62
|
+
if organization is None:
|
|
63
|
+
raise Exception("Organization not found")
|
|
64
|
+
account_name = organization.get("name")
|
|
65
|
+
return f"https://{self.domain}.uipath.com/{account_name}/{self.selected_tenant}/orchestrator_"
|
|
66
|
+
|
|
67
|
+
def post_refresh_token_request(self, refresh_token: str) -> TokenData:
|
|
68
|
+
url = f"https://{self.domain}.uipath.com/identity_/connect/token"
|
|
69
|
+
client_id = get_auth_config().get("client_id")
|
|
70
|
+
|
|
71
|
+
data = {
|
|
72
|
+
"grant_type": "refresh_token",
|
|
73
|
+
"refresh_token": refresh_token,
|
|
74
|
+
"client_id": client_id,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
78
|
+
|
|
79
|
+
response = requests.post(url, data=data, headers=headers)
|
|
80
|
+
if response.ok:
|
|
81
|
+
return response.json()
|
|
82
|
+
elif response.status_code == 401:
|
|
83
|
+
raise Exception("Unauthorized")
|
|
84
|
+
else:
|
|
85
|
+
raise Exception(f"Failed to refresh token: {response.status_code}")
|
|
86
|
+
|
|
87
|
+
def ensure_valid_token(self):
|
|
88
|
+
"""Ensure the access token is valid and refresh it if necessary.
|
|
89
|
+
|
|
90
|
+
This function should be called when running CLI commands to verify authentication.
|
|
91
|
+
It checks if an auth file exists and contains a valid non-expired token.
|
|
92
|
+
If the token is expired, it will attempt to refresh it.
|
|
93
|
+
If no auth file exists, it will raise an exception.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
Exception: If no auth file exists or token refresh fails
|
|
97
|
+
"""
|
|
98
|
+
auth_data = get_auth_data()
|
|
99
|
+
claims = get_parsed_token_data(auth_data)
|
|
100
|
+
exp = claims.get("exp")
|
|
101
|
+
|
|
102
|
+
if exp is not None and float(exp) > time.time():
|
|
103
|
+
if not os.getenv("UIPATH_URL"):
|
|
104
|
+
tenants_and_organizations = self.get_tenants_and_organizations()
|
|
105
|
+
select_tenant(
|
|
106
|
+
self.domain if self.domain else "alpha", tenants_and_organizations
|
|
107
|
+
)
|
|
108
|
+
return auth_data.get("access_token")
|
|
109
|
+
|
|
110
|
+
refresh_token = auth_data.get("refresh_token")
|
|
111
|
+
if refresh_token is None:
|
|
112
|
+
raise Exception("Refresh token not found")
|
|
113
|
+
token_data = self.post_refresh_token_request(refresh_token)
|
|
114
|
+
update_auth_file(token_data)
|
|
115
|
+
|
|
116
|
+
self.access_token = token_data["access_token"]
|
|
117
|
+
self.prt_id = claims.get("prt_id")
|
|
118
|
+
|
|
119
|
+
updated_env_contents = {
|
|
120
|
+
"UIPATH_ACCESS_TOKEN": token_data["access_token"],
|
|
121
|
+
}
|
|
122
|
+
if not os.getenv("UIPATH_URL"):
|
|
123
|
+
tenants_and_organizations = self.get_tenants_and_organizations()
|
|
124
|
+
select_tenant(
|
|
125
|
+
self.domain if self.domain else "alpha", tenants_and_organizations
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
update_env_file(updated_env_contents)
|
|
129
|
+
|
|
130
|
+
def has_initialized_auth(self):
|
|
131
|
+
try:
|
|
132
|
+
auth_data = get_auth_data()
|
|
133
|
+
if not auth_data or "access_token" not in auth_data:
|
|
134
|
+
return False
|
|
135
|
+
if not os.path.exists(".env"):
|
|
136
|
+
return False
|
|
137
|
+
if not os.getenv("UIPATH_ACCESS_TOKEN"):
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
return True
|
|
141
|
+
except Exception:
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def select_tenant(
|
|
146
|
+
domain: str, tenants_and_organizations: TenantsAndOrganizationInfoResponse
|
|
147
|
+
):
|
|
148
|
+
tenant_names = [tenant["name"] for tenant in tenants_and_organizations["tenants"]]
|
|
149
|
+
click.echo("Available tenants:")
|
|
150
|
+
for idx, name in enumerate(tenant_names):
|
|
151
|
+
click.echo(f" {idx}: {name}")
|
|
152
|
+
tenant_idx = click.prompt("Select tenant", type=int)
|
|
153
|
+
tenant_name = tenant_names[tenant_idx]
|
|
154
|
+
account_name = tenants_and_organizations["organization"]["name"]
|
|
155
|
+
click.echo(f"Selected tenant: {tenant_name}")
|
|
156
|
+
|
|
157
|
+
update_env_file(
|
|
158
|
+
{
|
|
159
|
+
"UIPATH_URL": f"https://{domain if domain else 'alpha'}.uipath.com/{account_name}/{tenant_name}",
|
|
160
|
+
"UIPATH_TENANT_ID": tenants_and_organizations["tenants"][tenant_idx]["id"],
|
|
161
|
+
"UIPATH_ORG_ID": tenants_and_organizations["organization"]["id"],
|
|
162
|
+
}
|
|
163
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ._models import AccessTokenData, TokenData
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def update_auth_file(token_data: TokenData):
|
|
11
|
+
os.makedirs(Path.cwd() / ".uipath", exist_ok=True)
|
|
12
|
+
auth_file = Path.cwd() / ".uipath" / ".auth.json"
|
|
13
|
+
with open(auth_file, "w") as f:
|
|
14
|
+
json.dump(token_data, f)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_auth_data() -> TokenData:
|
|
18
|
+
auth_file = Path.cwd() / ".uipath" / ".auth.json"
|
|
19
|
+
if not auth_file.exists():
|
|
20
|
+
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)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_parsed_token_data(token_data: Optional[TokenData] = None) -> AccessTokenData:
|
|
35
|
+
if not token_data:
|
|
36
|
+
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)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"client_id": "36dea5b8-e8bb-423d-8e7b-c808df8f1c00",
|
|
3
|
+
"redirect_uri": "https://localhost:__PY_REPLACE_PORT__/oidc/login",
|
|
4
|
+
"scope": "offline_access OrchestratorApiUserAccess ConnectionService DataService DocumentUnderstanding EnterpriseContextService Directory JamJamApi LLMGateway LLMOps OMS",
|
|
5
|
+
"port": 8104
|
|
6
|
+
}
|