uipath 2.1.14__py3-none-any.whl → 2.1.15__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/_push/sw_file_handler.py +464 -0
- uipath/_cli/_utils/_constants.py +2 -0
- uipath/_cli/_utils/_project_files.py +15 -3
- uipath/_cli/_utils/_studio_project.py +261 -40
- uipath/_cli/cli_init.py +3 -1
- uipath/_cli/cli_pack.py +7 -12
- uipath/_cli/cli_pull.py +28 -42
- uipath/_cli/cli_push.py +21 -459
- uipath/_services/_base_service.py +8 -0
- uipath/_services/api_client.py +8 -2
- uipath/_utils/constants.py +1 -0
- uipath/models/exceptions.py +6 -6
- {uipath-2.1.14.dist-info → uipath-2.1.15.dist-info}/METADATA +1 -1
- {uipath-2.1.14.dist-info → uipath-2.1.15.dist-info}/RECORD +17 -16
- {uipath-2.1.14.dist-info → uipath-2.1.15.dist-info}/WHEEL +0 -0
- {uipath-2.1.14.dist-info → uipath-2.1.15.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.14.dist-info → uipath-2.1.15.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,13 @@
|
|
1
|
-
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
from functools import wraps
|
4
|
+
from typing import Any, Callable, List, Optional, Union
|
2
5
|
|
3
|
-
import httpx
|
4
6
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
5
7
|
|
8
|
+
from uipath._utils.constants import HEADER_SW_LOCK_KEY
|
9
|
+
from uipath.models.exceptions import EnrichedException
|
10
|
+
|
6
11
|
|
7
12
|
class ProjectFile(BaseModel):
|
8
13
|
"""Model representing a file in a UiPath project.
|
@@ -139,45 +144,16 @@ class ProjectStructure(BaseModel):
|
|
139
144
|
return v
|
140
145
|
|
141
146
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
Makes an API call to fetch the complete file structure of a project,
|
151
|
-
including all files and folders. The response is validated against
|
152
|
-
the ProjectStructure model.
|
153
|
-
|
154
|
-
Args:
|
155
|
-
project_id: The ID of the project
|
156
|
-
base_url: The base URL for the API
|
157
|
-
token: Authentication token
|
158
|
-
client: HTTP client to use for requests
|
159
|
-
|
160
|
-
Returns:
|
161
|
-
ProjectStructure: The complete project structure
|
162
|
-
|
163
|
-
Raises:
|
164
|
-
httpx.HTTPError: If the API request fails
|
165
|
-
"""
|
166
|
-
headers = {"Authorization": f"Bearer {token}"}
|
167
|
-
url = (
|
168
|
-
f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure"
|
147
|
+
class LockInfo(BaseModel):
|
148
|
+
model_config = ConfigDict(
|
149
|
+
validate_by_name=True,
|
150
|
+
validate_by_alias=True,
|
151
|
+
use_enum_values=True,
|
152
|
+
arbitrary_types_allowed=True,
|
153
|
+
extra="allow",
|
169
154
|
)
|
170
|
-
|
171
|
-
|
172
|
-
response.raise_for_status()
|
173
|
-
return ProjectStructure.model_validate(response.json())
|
174
|
-
|
175
|
-
|
176
|
-
def download_file(base_url: str, file_id: str, client: httpx.Client, token: str) -> Any:
|
177
|
-
file_url = f"{base_url}/File/{file_id}"
|
178
|
-
response = client.get(file_url, headers={"Authorization": f"Bearer {token}"})
|
179
|
-
response.raise_for_status()
|
180
|
-
return response
|
155
|
+
project_lock_key: str = Field(alias="projectLockKey")
|
156
|
+
solution_lock_key: str = Field(alias="solutionLockKey")
|
181
157
|
|
182
158
|
|
183
159
|
def get_folder_by_name(
|
@@ -196,3 +172,248 @@ def get_folder_by_name(
|
|
196
172
|
if folder.name == folder_name:
|
197
173
|
return folder
|
198
174
|
return None
|
175
|
+
|
176
|
+
|
177
|
+
class AddedResource(BaseModel):
|
178
|
+
content_file_path: str
|
179
|
+
parent_path: Optional[str] = None
|
180
|
+
|
181
|
+
|
182
|
+
class ModifiedResource(BaseModel):
|
183
|
+
id: str
|
184
|
+
content_file_path: str
|
185
|
+
|
186
|
+
|
187
|
+
class StructuralMigration(BaseModel):
|
188
|
+
deleted_resources: List[str]
|
189
|
+
added_resources: List[AddedResource]
|
190
|
+
modified_resources: List[ModifiedResource]
|
191
|
+
|
192
|
+
|
193
|
+
def with_lock_retry(func: Callable[..., Any]) -> Callable[..., Any]:
|
194
|
+
@wraps(func)
|
195
|
+
async def wrapper(self, *args, **kwargs):
|
196
|
+
try:
|
197
|
+
lock_info = await self._retrieve_lock()
|
198
|
+
|
199
|
+
headers = kwargs.get("headers", {}) or {}
|
200
|
+
headers[HEADER_SW_LOCK_KEY] = lock_info.project_lock_key
|
201
|
+
kwargs["headers"] = headers
|
202
|
+
|
203
|
+
return await func(self, *args, **kwargs)
|
204
|
+
except EnrichedException as e:
|
205
|
+
if e.status_code == 423:
|
206
|
+
from uipath._cli._utils._console import ConsoleLogger
|
207
|
+
|
208
|
+
console = ConsoleLogger()
|
209
|
+
console.error(
|
210
|
+
"The project is temporarily locked. This could be due to modifications or active processes. Please wait a moment and try again."
|
211
|
+
)
|
212
|
+
raise
|
213
|
+
|
214
|
+
return wrapper
|
215
|
+
|
216
|
+
|
217
|
+
class StudioClient:
|
218
|
+
def __init__(self, project_id: str):
|
219
|
+
from uipath import UiPath
|
220
|
+
|
221
|
+
self.uipath: UiPath = UiPath()
|
222
|
+
self.file_operations_base_url: str = (
|
223
|
+
f"/studio_/backend/api/Project/{project_id}/FileOperations"
|
224
|
+
)
|
225
|
+
self._lock_operations_base_url: str = (
|
226
|
+
f"/studio_/backend/api/Project/{project_id}/Lock"
|
227
|
+
)
|
228
|
+
|
229
|
+
async def get_project_structure_async(self) -> ProjectStructure:
|
230
|
+
"""Retrieve the project's file structure from UiPath Cloud.
|
231
|
+
|
232
|
+
Makes an API call to fetch the complete file structure of a project,
|
233
|
+
including all files and folders. The response is validated against
|
234
|
+
the ProjectStructure model.
|
235
|
+
|
236
|
+
Returns:
|
237
|
+
ProjectStructure: The complete project structure
|
238
|
+
"""
|
239
|
+
response = await self.uipath.api_client.request_async(
|
240
|
+
"GET",
|
241
|
+
url=f"{self.file_operations_base_url}/Structure",
|
242
|
+
scoped="org",
|
243
|
+
)
|
244
|
+
|
245
|
+
return ProjectStructure.model_validate(response.json())
|
246
|
+
|
247
|
+
@with_lock_retry
|
248
|
+
async def create_folder_async(
|
249
|
+
self,
|
250
|
+
folder_name: str,
|
251
|
+
parent_id: Optional[str] = None,
|
252
|
+
headers: Optional[dict[str, Any]] = None,
|
253
|
+
) -> str:
|
254
|
+
"""Create a folder in the project.
|
255
|
+
|
256
|
+
Args:
|
257
|
+
folder_name: Name of the folder to create
|
258
|
+
parent_id: Optional parent folder ID
|
259
|
+
headers: HTTP headers (automatically injected by decorator)
|
260
|
+
|
261
|
+
Returns:
|
262
|
+
str: The created folder ID
|
263
|
+
"""
|
264
|
+
data = {"name": folder_name}
|
265
|
+
if parent_id:
|
266
|
+
data["parent_id"] = parent_id
|
267
|
+
response = await self.uipath.api_client.request_async(
|
268
|
+
"POST",
|
269
|
+
url=f"{self.file_operations_base_url}/Folder",
|
270
|
+
scoped="org",
|
271
|
+
json=data,
|
272
|
+
headers=headers or {},
|
273
|
+
)
|
274
|
+
return response.json()
|
275
|
+
|
276
|
+
async def download_file_async(self, file_id: str) -> Any:
|
277
|
+
response = await self.uipath.api_client.request_async(
|
278
|
+
"GET",
|
279
|
+
url=f"{self.file_operations_base_url}/File/{file_id}",
|
280
|
+
scoped="org",
|
281
|
+
)
|
282
|
+
return response
|
283
|
+
|
284
|
+
@with_lock_retry
|
285
|
+
async def upload_file_async(
|
286
|
+
self,
|
287
|
+
*,
|
288
|
+
local_file_path: Optional[str] = None,
|
289
|
+
file_content: Optional[str] = None,
|
290
|
+
file_name: str,
|
291
|
+
folder: Optional[ProjectFolder] = None,
|
292
|
+
remote_file: Optional[ProjectFile] = None,
|
293
|
+
headers: Optional[dict[str, Any]] = None,
|
294
|
+
) -> tuple[str, str]:
|
295
|
+
if local_file_path:
|
296
|
+
with open(local_file_path, "rb") as f:
|
297
|
+
file_content = f.read() # type: ignore
|
298
|
+
files_data = {"file": (file_name, file_content, "application/octet-stream")}
|
299
|
+
|
300
|
+
if remote_file:
|
301
|
+
# File exists in source_code folder, use PUT to update
|
302
|
+
response = await self.uipath.api_client.request_async(
|
303
|
+
"PUT",
|
304
|
+
files=files_data,
|
305
|
+
url=f"{self.file_operations_base_url}/File/{remote_file.id}",
|
306
|
+
scoped="org",
|
307
|
+
infer_content_type=True,
|
308
|
+
headers=headers or {},
|
309
|
+
)
|
310
|
+
action = "Updated"
|
311
|
+
else:
|
312
|
+
response = await self.uipath.api_client.request_async(
|
313
|
+
"POST",
|
314
|
+
url=f"{self.file_operations_base_url}/File",
|
315
|
+
data={"parentId": folder.id} if folder else None,
|
316
|
+
files=files_data,
|
317
|
+
scoped="org",
|
318
|
+
infer_content_type=True,
|
319
|
+
headers=headers or {},
|
320
|
+
)
|
321
|
+
action = "Uploaded"
|
322
|
+
|
323
|
+
# response contains only the uploaded file identifier
|
324
|
+
return response.json(), action
|
325
|
+
|
326
|
+
@with_lock_retry
|
327
|
+
async def delete_item_async(
|
328
|
+
self,
|
329
|
+
item_id: str,
|
330
|
+
headers: Optional[dict[str, Any]] = None,
|
331
|
+
) -> None:
|
332
|
+
await self.uipath.api_client.request_async(
|
333
|
+
"DELETE",
|
334
|
+
url=f"{self.file_operations_base_url}/Delete/{item_id}",
|
335
|
+
scoped="org",
|
336
|
+
headers=headers or {},
|
337
|
+
)
|
338
|
+
|
339
|
+
@with_lock_retry
|
340
|
+
async def perform_structural_migration_async(
|
341
|
+
self,
|
342
|
+
structural_migration: StructuralMigration,
|
343
|
+
headers: Optional[dict[str, Any]] = None,
|
344
|
+
) -> Any:
|
345
|
+
"""Perform structural migration of project files.
|
346
|
+
|
347
|
+
Args:
|
348
|
+
structural_migration: The structural migration data containing deleted and added resources
|
349
|
+
headers: HTTP headers (automatically injected by decorator)
|
350
|
+
|
351
|
+
Returns:
|
352
|
+
Any: The API response
|
353
|
+
"""
|
354
|
+
files: Any = []
|
355
|
+
deleted_resources_json = json.dumps(structural_migration.deleted_resources)
|
356
|
+
|
357
|
+
files.append(
|
358
|
+
(
|
359
|
+
"DeletedResources",
|
360
|
+
(None, deleted_resources_json),
|
361
|
+
)
|
362
|
+
)
|
363
|
+
|
364
|
+
for i, added_resource in enumerate(structural_migration.added_resources):
|
365
|
+
if os.path.exists(added_resource.content_file_path):
|
366
|
+
with open(added_resource.content_file_path, "rb") as f:
|
367
|
+
content = f.read()
|
368
|
+
|
369
|
+
filename = os.path.basename(added_resource.content_file_path)
|
370
|
+
files.append((f"AddedResources[{i}].Content", (filename, content)))
|
371
|
+
|
372
|
+
if added_resource.parent_path:
|
373
|
+
files.append(
|
374
|
+
(
|
375
|
+
f"AddedResources[{i}].ParentPath",
|
376
|
+
(None, added_resource.parent_path),
|
377
|
+
)
|
378
|
+
)
|
379
|
+
else:
|
380
|
+
raise FileNotFoundError(
|
381
|
+
f"File not found: {added_resource.content_file_path}"
|
382
|
+
)
|
383
|
+
|
384
|
+
for i, modified_resource in enumerate(structural_migration.modified_resources):
|
385
|
+
if os.path.exists(modified_resource.content_file_path):
|
386
|
+
with open(modified_resource.content_file_path, "rb") as f:
|
387
|
+
content = f.read()
|
388
|
+
|
389
|
+
filename = os.path.basename(modified_resource.content_file_path)
|
390
|
+
files.append((f"ModifiedResources[{i}].Content", (filename, content)))
|
391
|
+
files.append(
|
392
|
+
(
|
393
|
+
f"ModifiedResources[{i}].Id",
|
394
|
+
(None, modified_resource.id),
|
395
|
+
)
|
396
|
+
)
|
397
|
+
else:
|
398
|
+
raise FileNotFoundError(
|
399
|
+
f"File not found: {modified_resource.content_file_path}"
|
400
|
+
)
|
401
|
+
|
402
|
+
response = await self.uipath.api_client.request_async(
|
403
|
+
"POST",
|
404
|
+
url=f"{self.file_operations_base_url}/StructuralMigration",
|
405
|
+
scoped="org",
|
406
|
+
files=files,
|
407
|
+
infer_content_type=True,
|
408
|
+
headers=headers or {},
|
409
|
+
)
|
410
|
+
|
411
|
+
return response
|
412
|
+
|
413
|
+
async def _retrieve_lock(self) -> LockInfo:
|
414
|
+
response = await self.uipath.api_client.request_async(
|
415
|
+
"GET",
|
416
|
+
url=f"{self._lock_operations_base_url}",
|
417
|
+
scoped="org",
|
418
|
+
)
|
419
|
+
return LockInfo.model_validate(response.json())
|
uipath/_cli/cli_init.py
CHANGED
@@ -39,7 +39,9 @@ def create_telemetry_config_file(target_directory: str) -> None:
|
|
39
39
|
return
|
40
40
|
|
41
41
|
os.makedirs(uipath_dir, exist_ok=True)
|
42
|
-
telemetry_data = {
|
42
|
+
telemetry_data = {
|
43
|
+
_PROJECT_KEY: os.getenv("UIPATH_PROJECT_ID", None) or str(uuid.uuid4())
|
44
|
+
}
|
43
45
|
|
44
46
|
with open(telemetry_file, "w") as f:
|
45
47
|
json.dump(telemetry_data, f, indent=4)
|
uipath/_cli/cli_pack.py
CHANGED
@@ -7,6 +7,8 @@ from string import Template
|
|
7
7
|
|
8
8
|
import click
|
9
9
|
|
10
|
+
from uipath._cli._utils._constants import UIPATH_PROJECT_ID
|
11
|
+
|
10
12
|
from ..telemetry import track
|
11
13
|
from ..telemetry._constants import _PROJECT_KEY, _TELEMETRY_CONFIG_FILE
|
12
14
|
from ._utils._console import ConsoleLogger
|
@@ -30,6 +32,10 @@ def get_project_id() -> str:
|
|
30
32
|
Returns:
|
31
33
|
Project ID string (either from telemetry file or newly generated).
|
32
34
|
"""
|
35
|
+
# first check if this is a studio project
|
36
|
+
if os.getenv(UIPATH_PROJECT_ID, None):
|
37
|
+
return os.getenv(UIPATH_PROJECT_ID)
|
38
|
+
|
33
39
|
telemetry_file = os.path.join(".uipath", _TELEMETRY_CONFIG_FILE)
|
34
40
|
|
35
41
|
if os.path.exists(telemetry_file):
|
@@ -247,7 +253,7 @@ def pack_fn(
|
|
247
253
|
z.writestr(f"{projectName}.nuspec", nuspec_content)
|
248
254
|
z.writestr("_rels/.rels", rels_content)
|
249
255
|
|
250
|
-
files = files_to_include(config_data, directory)
|
256
|
+
files = files_to_include(config_data, directory, include_uv_lock)
|
251
257
|
|
252
258
|
for file in files:
|
253
259
|
if file.is_binary:
|
@@ -269,17 +275,6 @@ def pack_fn(
|
|
269
275
|
with open(file.file_path, "r", encoding="latin-1") as f:
|
270
276
|
z.writestr(f"content/{file.relative_path}", f.read())
|
271
277
|
|
272
|
-
if include_uv_lock:
|
273
|
-
file = "uv.lock"
|
274
|
-
file_path = os.path.join(directory, file)
|
275
|
-
if os.path.exists(file_path):
|
276
|
-
try:
|
277
|
-
with open(file_path, "r", encoding="utf-8") as f:
|
278
|
-
z.writestr(f"content/{file}", f.read())
|
279
|
-
except UnicodeDecodeError:
|
280
|
-
with open(file_path, "r", encoding="latin-1") as f:
|
281
|
-
z.writestr(f"content/{file}", f.read())
|
282
|
-
|
283
278
|
|
284
279
|
def display_project_info(config):
|
285
280
|
max_label_length = max(
|
uipath/_cli/cli_pull.py
CHANGED
@@ -10,25 +10,23 @@ It handles:
|
|
10
10
|
"""
|
11
11
|
|
12
12
|
# type: ignore
|
13
|
+
import asyncio
|
13
14
|
import hashlib
|
14
15
|
import json
|
15
16
|
import os
|
16
17
|
from typing import Dict, Set
|
17
18
|
|
18
19
|
import click
|
19
|
-
import httpx
|
20
20
|
from dotenv import load_dotenv
|
21
21
|
|
22
|
-
from .._utils._ssl_context import get_httpx_client_kwargs
|
23
22
|
from ..telemetry import track
|
24
|
-
from ._utils._common import get_env_vars, get_org_scoped_url
|
25
23
|
from ._utils._console import ConsoleLogger
|
24
|
+
from ._utils._constants import UIPATH_PROJECT_ID
|
26
25
|
from ._utils._studio_project import (
|
27
26
|
ProjectFile,
|
28
27
|
ProjectFolder,
|
29
|
-
|
28
|
+
StudioClient,
|
30
29
|
get_folder_by_name,
|
31
|
-
get_project_structure,
|
32
30
|
)
|
33
31
|
|
34
32
|
console = ConsoleLogger()
|
@@ -76,22 +74,18 @@ def collect_files_from_folder(
|
|
76
74
|
collect_files_from_folder(subfolder, subfolder_path, files_dict)
|
77
75
|
|
78
76
|
|
79
|
-
def download_folder_files(
|
77
|
+
async def download_folder_files(
|
78
|
+
studio_client: StudioClient,
|
80
79
|
folder: ProjectFolder,
|
81
80
|
base_path: str,
|
82
|
-
base_url: str,
|
83
|
-
token: str,
|
84
|
-
client: httpx.Client,
|
85
81
|
processed_files: Set[str],
|
86
82
|
) -> None:
|
87
83
|
"""Download files from a folder recursively.
|
88
84
|
|
89
85
|
Args:
|
86
|
+
studio_client: Studio client
|
90
87
|
folder: The folder to download files from
|
91
88
|
base_path: Base path for local file storage
|
92
|
-
base_url: Base URL for API requests
|
93
|
-
token: Authentication token
|
94
|
-
client: HTTP client
|
95
89
|
processed_files: Set to track processed files
|
96
90
|
"""
|
97
91
|
files_dict: Dict[str, ProjectFile] = {}
|
@@ -106,7 +100,7 @@ def download_folder_files(
|
|
106
100
|
os.makedirs(local_dir)
|
107
101
|
|
108
102
|
# Download remote file
|
109
|
-
response =
|
103
|
+
response = await studio_client.download_file_async(remote_file.id)
|
110
104
|
remote_content = response.read().decode("utf-8")
|
111
105
|
remote_hash = compute_normalized_hash(remote_content)
|
112
106
|
|
@@ -163,53 +157,45 @@ def pull(root: str) -> None:
|
|
163
157
|
$ uipath pull
|
164
158
|
$ uipath pull /path/to/project
|
165
159
|
"""
|
166
|
-
if not os.getenv(
|
160
|
+
if not (project_id := os.getenv(UIPATH_PROJECT_ID, False)):
|
167
161
|
console.error("UIPATH_PROJECT_ID environment variable not found.")
|
168
162
|
|
169
|
-
|
170
|
-
base_api_url = f"{get_org_scoped_url(base_url)}/studio_/backend/api/Project/{os.getenv('UIPATH_PROJECT_ID')}/FileOperations"
|
163
|
+
studio_client = StudioClient(project_id)
|
171
164
|
|
172
165
|
with console.spinner("Pulling UiPath project files..."):
|
173
166
|
try:
|
174
|
-
|
175
|
-
# Get project structure
|
176
|
-
structure = get_project_structure(
|
177
|
-
os.getenv("UIPATH_PROJECT_ID"), # type: ignore
|
178
|
-
get_org_scoped_url(base_url),
|
179
|
-
token,
|
180
|
-
client,
|
181
|
-
)
|
167
|
+
structure = asyncio.run(studio_client.get_project_structure_async())
|
182
168
|
|
183
|
-
|
169
|
+
processed_files: Set[str] = set()
|
184
170
|
|
185
|
-
|
186
|
-
|
187
|
-
|
171
|
+
# Process source_code folder
|
172
|
+
source_code_folder = get_folder_by_name(structure, "source_code")
|
173
|
+
if source_code_folder:
|
174
|
+
asyncio.run(
|
188
175
|
download_folder_files(
|
176
|
+
studio_client,
|
189
177
|
source_code_folder,
|
190
178
|
root,
|
191
|
-
base_api_url,
|
192
|
-
token,
|
193
|
-
client,
|
194
179
|
processed_files,
|
195
180
|
)
|
196
|
-
|
197
|
-
|
181
|
+
)
|
182
|
+
else:
|
183
|
+
console.warning("No source_code folder found in remote project")
|
198
184
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
185
|
+
# Process evals folder
|
186
|
+
evals_folder = get_folder_by_name(structure, "evals")
|
187
|
+
if evals_folder:
|
188
|
+
evals_path = os.path.join(root, "evals")
|
189
|
+
asyncio.run(
|
203
190
|
download_folder_files(
|
191
|
+
studio_client,
|
204
192
|
evals_folder,
|
205
193
|
evals_path,
|
206
|
-
base_api_url,
|
207
|
-
token,
|
208
|
-
client,
|
209
194
|
processed_files,
|
210
195
|
)
|
211
|
-
|
212
|
-
|
196
|
+
)
|
197
|
+
else:
|
198
|
+
console.warning("No evals folder found in remote project")
|
213
199
|
|
214
200
|
except Exception as e:
|
215
201
|
console.error(f"Failed to pull UiPath project: {str(e)}")
|