uipath 2.1.4__py3-none-any.whl → 2.1.6__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.
- uipath/_cli/__init__.py +2 -0
- uipath/_cli/_auth/auth_config.json +2 -2
- uipath/_cli/_utils/_constants.py +6 -0
- uipath/_cli/_utils/_project_files.py +361 -0
- uipath/_cli/_utils/_studio_project.py +138 -0
- uipath/_cli/_utils/_uv_helpers.py +114 -0
- uipath/_cli/cli_pack.py +42 -341
- uipath/_cli/cli_push.py +546 -0
- uipath/_services/llm_gateway_service.py +149 -13
- {uipath-2.1.4.dist-info → uipath-2.1.6.dist-info}/METADATA +1 -1
- {uipath-2.1.4.dist-info → uipath-2.1.6.dist-info}/RECORD +14 -10
- {uipath-2.1.4.dist-info → uipath-2.1.6.dist-info}/WHEEL +0 -0
- {uipath-2.1.4.dist-info → uipath-2.1.6.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.4.dist-info → uipath-2.1.6.dist-info}/licenses/LICENSE +0 -0
uipath/_cli/cli_push.py
ADDED
@@ -0,0 +1,546 @@
|
|
1
|
+
# type: ignore
|
2
|
+
"""CLI command for pushing local project files to UiPath StudioWeb solution.
|
3
|
+
|
4
|
+
This module provides functionality to push local project files to a UiPath StudioWeb solution.
|
5
|
+
It handles:
|
6
|
+
- File uploads and updates
|
7
|
+
- File deletions for removed local files
|
8
|
+
- Optional UV lock file management
|
9
|
+
- Project structure pushing
|
10
|
+
|
11
|
+
The push process ensures that the remote project structure matches the local files,
|
12
|
+
taking into account:
|
13
|
+
- Entry point files from uipath.json
|
14
|
+
- Project configuration from pyproject.toml
|
15
|
+
- Optional UV lock file for dependency management
|
16
|
+
"""
|
17
|
+
|
18
|
+
import json
|
19
|
+
import os
|
20
|
+
from datetime import datetime, timezone
|
21
|
+
from typing import Any, Dict, Optional, Set, Tuple
|
22
|
+
from urllib.parse import urlparse
|
23
|
+
|
24
|
+
import click
|
25
|
+
import httpx
|
26
|
+
import jwt
|
27
|
+
from dotenv import load_dotenv
|
28
|
+
|
29
|
+
from .._utils._ssl_context import get_httpx_client_kwargs
|
30
|
+
from ..telemetry import track
|
31
|
+
from ._utils._common import get_env_vars
|
32
|
+
from ._utils._console import ConsoleLogger
|
33
|
+
from ._utils._constants import (
|
34
|
+
AGENT_INITIAL_CODE_VERSION,
|
35
|
+
AGENT_STORAGE_VERSION,
|
36
|
+
AGENT_TARGET_RUNTIME,
|
37
|
+
AGENT_VERSION,
|
38
|
+
)
|
39
|
+
from ._utils._project_files import (
|
40
|
+
ensure_config_file,
|
41
|
+
files_to_include,
|
42
|
+
get_project_config,
|
43
|
+
read_toml_project,
|
44
|
+
validate_config,
|
45
|
+
)
|
46
|
+
from ._utils._studio_project import ProjectFile, ProjectFolder, ProjectStructure
|
47
|
+
from ._utils._uv_helpers import handle_uv_operations
|
48
|
+
|
49
|
+
console = ConsoleLogger()
|
50
|
+
load_dotenv(override=True)
|
51
|
+
|
52
|
+
|
53
|
+
def get_author_from_token_or_toml(directory: str) -> str:
|
54
|
+
"""Extract preferred_username from JWT token or fall back to pyproject.toml author.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
directory: Project directory containing pyproject.toml
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
str: Author name from JWT preferred_username or pyproject.toml authors field
|
61
|
+
"""
|
62
|
+
# Try to get author from JWT token first
|
63
|
+
token = os.getenv("UIPATH_ACCESS_TOKEN")
|
64
|
+
if token:
|
65
|
+
try:
|
66
|
+
decoded_token = jwt.decode(token, options={"verify_signature": False})
|
67
|
+
preferred_username = decoded_token.get("preferred_username")
|
68
|
+
if preferred_username:
|
69
|
+
return preferred_username
|
70
|
+
except Exception:
|
71
|
+
# If JWT decoding fails, fall back to toml
|
72
|
+
pass
|
73
|
+
|
74
|
+
toml_data = read_toml_project(os.path.join(directory, "pyproject.toml"))
|
75
|
+
return toml_data.get("authors", "").strip()
|
76
|
+
|
77
|
+
|
78
|
+
def get_org_scoped_url(base_url: str) -> str:
|
79
|
+
parsed = urlparse(base_url)
|
80
|
+
org_name, *_ = parsed.path.strip("/").split("/")
|
81
|
+
|
82
|
+
# Construct the new scoped URL (scheme + domain + org_name)
|
83
|
+
org_scoped_url = f"{parsed.scheme}://{parsed.netloc}/{org_name}"
|
84
|
+
return org_scoped_url
|
85
|
+
|
86
|
+
|
87
|
+
def get_project_structure(
|
88
|
+
project_id: str,
|
89
|
+
base_url: str,
|
90
|
+
token: str,
|
91
|
+
client: httpx.Client,
|
92
|
+
) -> ProjectStructure:
|
93
|
+
"""Retrieve the project's file structure from UiPath Cloud.
|
94
|
+
|
95
|
+
Makes an API call to fetch the complete file structure of a project,
|
96
|
+
including all files and folders. The response is validated against
|
97
|
+
the ProjectStructure model.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
project_id: The ID of the project
|
101
|
+
base_url: The base URL for the API
|
102
|
+
token: Authentication token
|
103
|
+
client: HTTP client to use for requests
|
104
|
+
|
105
|
+
Returns:
|
106
|
+
ProjectStructure: The complete project structure
|
107
|
+
|
108
|
+
Raises:
|
109
|
+
httpx.HTTPError: If the API request fails
|
110
|
+
"""
|
111
|
+
url = get_org_scoped_url(base_url)
|
112
|
+
headers = {"Authorization": f"Bearer {token}"}
|
113
|
+
url = f"{url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure"
|
114
|
+
|
115
|
+
response = client.get(url, headers=headers)
|
116
|
+
response.raise_for_status()
|
117
|
+
return ProjectStructure.model_validate(response.json())
|
118
|
+
|
119
|
+
|
120
|
+
def collect_all_files(
|
121
|
+
folder: ProjectFolder, files_dict: Dict[str, ProjectFile]
|
122
|
+
) -> None:
|
123
|
+
"""Recursively collect all files from a folder and its subfolders.
|
124
|
+
|
125
|
+
Traverses the folder structure recursively and adds all files to the
|
126
|
+
provided dictionary, using the file name as the key.
|
127
|
+
"""
|
128
|
+
# Add files from current folder
|
129
|
+
for file in folder.files:
|
130
|
+
files_dict[file.name] = file
|
131
|
+
|
132
|
+
# Recursively process subfolders
|
133
|
+
for subfolder in folder.folders:
|
134
|
+
collect_all_files(subfolder, files_dict)
|
135
|
+
|
136
|
+
|
137
|
+
def get_folder_by_name(
|
138
|
+
structure: ProjectStructure, folder_name: str
|
139
|
+
) -> Optional[ProjectFolder]:
|
140
|
+
"""Get a folder from the project structure by name.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
structure: The project structure
|
144
|
+
folder_name: Name of the folder to find
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
Optional[ProjectFolder]: The found folder or None
|
148
|
+
"""
|
149
|
+
for folder in structure.folders:
|
150
|
+
if folder.name == folder_name:
|
151
|
+
return folder
|
152
|
+
return None
|
153
|
+
|
154
|
+
|
155
|
+
def get_all_remote_files(
|
156
|
+
structure: ProjectStructure, source_code_folder: Optional[ProjectFolder] = None
|
157
|
+
) -> Tuple[Dict[str, ProjectFile], Dict[str, ProjectFile]]:
|
158
|
+
"""Get all files from the project structure indexed by name.
|
159
|
+
|
160
|
+
Creates two flat dictionaries of files in the project:
|
161
|
+
1. Root level files
|
162
|
+
2. Files in the source_code folder (if exists)
|
163
|
+
|
164
|
+
Args:
|
165
|
+
structure: The project structure
|
166
|
+
source_code_folder: Optional source_code folder to collect files from
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
Tuple[Dict[str, ProjectFile], Dict[str, ProjectFile]]:
|
170
|
+
(root_files, source_code_files)
|
171
|
+
"""
|
172
|
+
root_files: Dict[str, ProjectFile] = {}
|
173
|
+
source_code_files: Dict[str, ProjectFile] = {}
|
174
|
+
|
175
|
+
# Add files from root level
|
176
|
+
for file in structure.files:
|
177
|
+
root_files[file.name] = file
|
178
|
+
|
179
|
+
# Add files from source_code folder if it exists
|
180
|
+
if source_code_folder:
|
181
|
+
collect_all_files(source_code_folder, source_code_files)
|
182
|
+
|
183
|
+
return root_files, source_code_files
|
184
|
+
|
185
|
+
|
186
|
+
def delete_remote_file(
|
187
|
+
project_id: str, file_id: str, base_url: str, token: str, client: httpx.Client
|
188
|
+
) -> None:
|
189
|
+
"""Delete a file from the remote project.
|
190
|
+
|
191
|
+
Makes an API call to delete a specific file from the UiPath Cloud project.
|
192
|
+
|
193
|
+
Args:
|
194
|
+
project_id: The ID of the project
|
195
|
+
file_id: The ID of the file to delete
|
196
|
+
base_url: The base URL for the API
|
197
|
+
token: Authentication token
|
198
|
+
client: HTTP client to use for the request
|
199
|
+
|
200
|
+
Raises:
|
201
|
+
httpx.HTTPError: If the API request fails
|
202
|
+
"""
|
203
|
+
url = get_org_scoped_url(base_url)
|
204
|
+
headers = {"Authorization": f"Bearer {token}"}
|
205
|
+
url = f"{url}/studio_/backend/api/Project/{project_id}/FileOperations/Delete/{file_id}"
|
206
|
+
|
207
|
+
response = client.delete(url, headers=headers)
|
208
|
+
response.raise_for_status()
|
209
|
+
|
210
|
+
|
211
|
+
def update_agent_json(
|
212
|
+
project_id: str,
|
213
|
+
base_url: str,
|
214
|
+
token: str,
|
215
|
+
directory: str,
|
216
|
+
client: httpx.Client,
|
217
|
+
processed_files: Optional[Set[str]] = None,
|
218
|
+
agent_json_file: Optional[ProjectFile] = None,
|
219
|
+
) -> None:
|
220
|
+
"""Update agent.json file with metadata from uipath.json.
|
221
|
+
|
222
|
+
This function:
|
223
|
+
1. Downloads existing agent.json if it exists
|
224
|
+
2. Updates metadata based on uipath.json content
|
225
|
+
3. Increments code version
|
226
|
+
4. Updates author from JWT or pyproject.toml
|
227
|
+
5. Uploads updated agent.json
|
228
|
+
|
229
|
+
Args:
|
230
|
+
project_id: The ID of the project
|
231
|
+
base_url: The base URL for the API
|
232
|
+
token: Authentication token
|
233
|
+
directory: Project root directory
|
234
|
+
client: HTTP client to use for requests
|
235
|
+
processed_files: Optional set to track processed files
|
236
|
+
agent_json_file: Optional existing agent.json file
|
237
|
+
|
238
|
+
Raises:
|
239
|
+
httpx.HTTPError: If API requests fail
|
240
|
+
FileNotFoundError: If required files are missing
|
241
|
+
json.JSONDecodeError: If JSON parsing fails
|
242
|
+
"""
|
243
|
+
url = get_org_scoped_url(base_url)
|
244
|
+
headers = {"Authorization": f"Bearer {token}"}
|
245
|
+
|
246
|
+
# Read uipath.json
|
247
|
+
with open(os.path.join(directory, "uipath.json"), "r") as f:
|
248
|
+
uipath_config = json.load(f)
|
249
|
+
|
250
|
+
try:
|
251
|
+
entrypoints = [
|
252
|
+
{"input": entry_point["input"], "output": entry_point["output"]}
|
253
|
+
for entry_point in uipath_config["entryPoints"]
|
254
|
+
]
|
255
|
+
except (FileNotFoundError, KeyError) as e:
|
256
|
+
console.error(
|
257
|
+
f"Unable to extract entrypoints from configuration file. Please run 'uipath init' : {str(e)}",
|
258
|
+
)
|
259
|
+
|
260
|
+
author = get_author_from_token_or_toml(directory)
|
261
|
+
|
262
|
+
# Initialize agent.json structure
|
263
|
+
agent_json = {
|
264
|
+
"version": AGENT_VERSION,
|
265
|
+
"metadata": {
|
266
|
+
"storageVersion": AGENT_STORAGE_VERSION,
|
267
|
+
"targetRuntime": AGENT_TARGET_RUNTIME,
|
268
|
+
"isConversational": False,
|
269
|
+
"codeVersion": AGENT_INITIAL_CODE_VERSION,
|
270
|
+
"author": author,
|
271
|
+
"pushDate": datetime.now(timezone.utc).isoformat(),
|
272
|
+
},
|
273
|
+
"entryPoints": entrypoints,
|
274
|
+
"bindings": uipath_config.get("bindings", {"version": "2.0", "resources": []}),
|
275
|
+
}
|
276
|
+
|
277
|
+
base_api_url = f"{url}/studio_/backend/api/Project/{project_id}/FileOperations"
|
278
|
+
if agent_json_file:
|
279
|
+
# Download existing agent.json
|
280
|
+
file_url = f"{base_api_url}/File/{agent_json_file.id}"
|
281
|
+
response = client.get(file_url, headers=headers)
|
282
|
+
response.raise_for_status()
|
283
|
+
|
284
|
+
try:
|
285
|
+
existing_agent = response.json()
|
286
|
+
# Get current version and increment patch version
|
287
|
+
version_parts = existing_agent["metadata"]["codeVersion"].split(".")
|
288
|
+
if len(version_parts) >= 3:
|
289
|
+
version_parts[-1] = str(int(version_parts[-1]) + 1)
|
290
|
+
agent_json["metadata"]["codeVersion"] = ".".join(version_parts)
|
291
|
+
else:
|
292
|
+
# If version format is invalid, start from initial version + 1
|
293
|
+
agent_json["metadata"]["codeVersion"] = (
|
294
|
+
AGENT_INITIAL_CODE_VERSION[:-1] + "1"
|
295
|
+
)
|
296
|
+
except (json.JSONDecodeError, KeyError, ValueError):
|
297
|
+
console.warning(
|
298
|
+
"Could not parse existing agent.json, using default version"
|
299
|
+
)
|
300
|
+
|
301
|
+
# Upload updated agent.json
|
302
|
+
files_data = {"file": ("agent.json", json.dumps(agent_json), "application/json")}
|
303
|
+
|
304
|
+
# if agent.json already exists update it, otherwise upload it
|
305
|
+
if agent_json_file:
|
306
|
+
url = f"{base_api_url}/File/{agent_json_file.id}"
|
307
|
+
response = client.put(url, files=files_data, headers=headers)
|
308
|
+
else:
|
309
|
+
url = f"{base_api_url}/File"
|
310
|
+
response = client.post(url, files=files_data, headers=headers)
|
311
|
+
|
312
|
+
response.raise_for_status()
|
313
|
+
console.success(f"Updated {click.style('agent.json', fg='cyan')}")
|
314
|
+
|
315
|
+
# Mark agent.json as processed to prevent deletion
|
316
|
+
if processed_files is not None:
|
317
|
+
processed_files.add("agent.json")
|
318
|
+
|
319
|
+
|
320
|
+
def create_project_folder(
|
321
|
+
project_id: str,
|
322
|
+
folder_name: str,
|
323
|
+
base_url: str,
|
324
|
+
token: str,
|
325
|
+
client: httpx.Client,
|
326
|
+
) -> ProjectFolder:
|
327
|
+
"""Create a new folder in the project.
|
328
|
+
|
329
|
+
Args:
|
330
|
+
project_id: The ID of the project
|
331
|
+
folder_name: Name of the folder to create
|
332
|
+
base_url: The base URL for the API
|
333
|
+
token: Authentication token
|
334
|
+
client: HTTP client to use for requests
|
335
|
+
|
336
|
+
Returns:
|
337
|
+
ProjectFolder: The created folder object
|
338
|
+
|
339
|
+
Raises:
|
340
|
+
httpx.HTTPError: If the API request fails
|
341
|
+
"""
|
342
|
+
url = get_org_scoped_url(base_url)
|
343
|
+
headers = {"Authorization": f"Bearer {token}"}
|
344
|
+
url = f"{url}/studio_/backend/api/Project/{project_id}/FileOperations/Folder"
|
345
|
+
|
346
|
+
data = {"name": folder_name}
|
347
|
+
response = client.post(url, json=data, headers=headers)
|
348
|
+
response.raise_for_status()
|
349
|
+
return ProjectFolder(name="source_code", id=response.content.decode("utf-8"))
|
350
|
+
|
351
|
+
|
352
|
+
def upload_source_files_to_project(
|
353
|
+
project_id: str,
|
354
|
+
config_data: dict[Any, str],
|
355
|
+
directory: str,
|
356
|
+
base_url: str,
|
357
|
+
token: str,
|
358
|
+
include_uv_lock: bool = True,
|
359
|
+
) -> None:
|
360
|
+
"""Upload source files to UiPath project.
|
361
|
+
|
362
|
+
This function handles the pushing of local files to the remote project:
|
363
|
+
- Updates existing files that have changed
|
364
|
+
- Uploads new files that don't exist remotely
|
365
|
+
- Deletes remote files that no longer exist locally
|
366
|
+
- Optionally includes the UV lock file
|
367
|
+
"""
|
368
|
+
files = [
|
369
|
+
file.file_path.replace("./", "", 1)
|
370
|
+
for file in files_to_include(config_data, directory)
|
371
|
+
]
|
372
|
+
if include_uv_lock:
|
373
|
+
files.append("uv.lock")
|
374
|
+
|
375
|
+
url = get_org_scoped_url(base_url)
|
376
|
+
headers = {"Authorization": f"Bearer {token}"}
|
377
|
+
base_api_url = f"{url}/studio_/backend/api/Project/{project_id}/FileOperations"
|
378
|
+
|
379
|
+
with httpx.Client(**get_httpx_client_kwargs()) as client:
|
380
|
+
# get existing project structure
|
381
|
+
try:
|
382
|
+
structure = get_project_structure(project_id, base_url, token, client)
|
383
|
+
source_code_folder = get_folder_by_name(structure, "source_code")
|
384
|
+
root_files, source_code_files = get_all_remote_files(
|
385
|
+
structure, source_code_folder
|
386
|
+
)
|
387
|
+
except Exception as e:
|
388
|
+
console.error(f"Failed to get project structure: {str(e)}")
|
389
|
+
raise
|
390
|
+
|
391
|
+
# keep track of processed files to identify which ones to delete later
|
392
|
+
processed_root_files: Set[str] = set()
|
393
|
+
processed_source_files: Set[str] = set()
|
394
|
+
|
395
|
+
# Create source_code folder if it doesn't exist
|
396
|
+
if not source_code_folder:
|
397
|
+
try:
|
398
|
+
source_code_folder = create_project_folder(
|
399
|
+
project_id, "source_code", base_url, token, client
|
400
|
+
)
|
401
|
+
console.success(
|
402
|
+
f"Created {click.style('source_code', fg='cyan')} folder"
|
403
|
+
)
|
404
|
+
source_code_files = {} # Initialize empty dict for new folder
|
405
|
+
except httpx.HTTPStatusError as http_err:
|
406
|
+
if http_err.response.status_code == 423:
|
407
|
+
console.error(
|
408
|
+
"Resource is locked. Unable to create 'source_code' folder."
|
409
|
+
)
|
410
|
+
raise
|
411
|
+
|
412
|
+
except Exception as e:
|
413
|
+
console.error(f"Failed to create 'source_code' folder: {str(e)}")
|
414
|
+
raise
|
415
|
+
|
416
|
+
# Update agent.json first at root level
|
417
|
+
try:
|
418
|
+
update_agent_json(
|
419
|
+
project_id,
|
420
|
+
base_url,
|
421
|
+
token,
|
422
|
+
directory,
|
423
|
+
client,
|
424
|
+
processed_root_files,
|
425
|
+
root_files.get("agent.json", None),
|
426
|
+
)
|
427
|
+
except Exception as e:
|
428
|
+
console.error(f"Failed to update agent.json: {str(e)}")
|
429
|
+
raise
|
430
|
+
|
431
|
+
# Continue with rest of files in source_code folder
|
432
|
+
for file_path in files:
|
433
|
+
try:
|
434
|
+
abs_path = os.path.abspath(os.path.join(directory, file_path))
|
435
|
+
if not os.path.exists(abs_path):
|
436
|
+
console.warning(
|
437
|
+
f"File not found: {click.style(abs_path, fg='cyan')}"
|
438
|
+
)
|
439
|
+
continue
|
440
|
+
|
441
|
+
file_name = os.path.basename(file_path)
|
442
|
+
|
443
|
+
# Skip agent.json as it's already handled
|
444
|
+
if file_name == "agent.json":
|
445
|
+
continue
|
446
|
+
|
447
|
+
remote_file = source_code_files.get(file_name)
|
448
|
+
processed_source_files.add(file_name)
|
449
|
+
|
450
|
+
with open(abs_path, "rb") as f:
|
451
|
+
files_data = {"file": (file_name, f, "application/octet-stream")}
|
452
|
+
form_data = {"parentId": source_code_folder.id}
|
453
|
+
|
454
|
+
if remote_file:
|
455
|
+
# File exists in source_code folder, use PUT to update
|
456
|
+
url = f"{base_api_url}/File/{remote_file.id}"
|
457
|
+
response = client.put(url, files=files_data, headers=headers)
|
458
|
+
action = "Updated"
|
459
|
+
else:
|
460
|
+
# File doesn't exist, use POST to create in source_code folder
|
461
|
+
url = f"{base_api_url}/File"
|
462
|
+
response = client.post(
|
463
|
+
url, files=files_data, data=form_data, headers=headers
|
464
|
+
)
|
465
|
+
action = "Uploaded"
|
466
|
+
|
467
|
+
response.raise_for_status()
|
468
|
+
console.success(f"{action} {click.style(file_path, fg='cyan')}")
|
469
|
+
|
470
|
+
except Exception as e:
|
471
|
+
console.error(
|
472
|
+
f"Failed to upload {click.style(file_path, fg='cyan')}: {str(e)}"
|
473
|
+
)
|
474
|
+
raise
|
475
|
+
|
476
|
+
# Delete files that no longer exist locally
|
477
|
+
if source_code_files:
|
478
|
+
for file_name, remote_file in source_code_files.items():
|
479
|
+
if file_name not in processed_source_files:
|
480
|
+
try:
|
481
|
+
delete_remote_file(
|
482
|
+
project_id, remote_file.id, base_url, token, client
|
483
|
+
)
|
484
|
+
console.success(
|
485
|
+
f"Deleted remote file {click.style(file_name, fg='cyan')}"
|
486
|
+
)
|
487
|
+
except Exception as e:
|
488
|
+
console.error(
|
489
|
+
f"Failed to delete remote file {click.style(file_name, fg='cyan')}: {str(e)}"
|
490
|
+
)
|
491
|
+
raise
|
492
|
+
|
493
|
+
|
494
|
+
@click.command()
|
495
|
+
@click.argument("root", type=str, default="./")
|
496
|
+
@click.option(
|
497
|
+
"--nolock",
|
498
|
+
is_flag=True,
|
499
|
+
help="Skip running uv lock and exclude uv.lock from the package",
|
500
|
+
)
|
501
|
+
@track
|
502
|
+
def push(root: str, nolock: bool) -> None:
|
503
|
+
"""Push local project files to Studio Web Project.
|
504
|
+
|
505
|
+
This command pushes the local project files to a UiPath Studio Web project.
|
506
|
+
It ensures that the remote project structure matches the local files by:
|
507
|
+
- Updating existing files that have changed
|
508
|
+
- Uploading new files
|
509
|
+
- Deleting remote files that no longer exist locally
|
510
|
+
- Optionally managing the UV lock file
|
511
|
+
|
512
|
+
Args:
|
513
|
+
root: The root directory of the project
|
514
|
+
nolock: Whether to skip UV lock operations and exclude uv.lock from push
|
515
|
+
|
516
|
+
Environment Variables:
|
517
|
+
UIPATH_PROJECT_ID: Required. The ID of the UiPath Cloud project
|
518
|
+
|
519
|
+
Example:
|
520
|
+
$ uipath push
|
521
|
+
$ uipath push --nolock
|
522
|
+
"""
|
523
|
+
ensure_config_file(root)
|
524
|
+
config = get_project_config(root)
|
525
|
+
validate_config(config)
|
526
|
+
|
527
|
+
if not os.getenv("UIPATH_PROJECT_ID", False):
|
528
|
+
console.error("UIPATH_PROJECT_ID environment variable not found.")
|
529
|
+
[base_url, token] = get_env_vars()
|
530
|
+
|
531
|
+
with console.spinner("Pushing coded UiPath project to Studio Web..."):
|
532
|
+
try:
|
533
|
+
# Handle uv operations before packaging, unless nolock is specified
|
534
|
+
if not nolock:
|
535
|
+
handle_uv_operations(root)
|
536
|
+
|
537
|
+
upload_source_files_to_project(
|
538
|
+
os.getenv("UIPATH_PROJECT_ID"),
|
539
|
+
config,
|
540
|
+
root,
|
541
|
+
base_url,
|
542
|
+
token,
|
543
|
+
include_uv_lock=not nolock,
|
544
|
+
)
|
545
|
+
except Exception:
|
546
|
+
console.error("Failed to push UiPath project")
|