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.
@@ -1,8 +1,13 @@
1
- from typing import Any, List, Optional, Union
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
- def get_project_structure(
143
- project_id: str,
144
- base_url: str,
145
- token: str,
146
- client: httpx.Client,
147
- ) -> ProjectStructure:
148
- """Retrieve the project's file structure from UiPath Cloud.
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
- response = client.get(url, headers=headers)
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 = {_PROJECT_KEY: str(uuid.uuid4())}
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
- download_file,
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 = download_file(base_url, remote_file.id, client, token)
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("UIPATH_PROJECT_ID", False):
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
- [base_url, token] = get_env_vars()
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
- with httpx.Client(**get_httpx_client_kwargs()) as client:
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
- processed_files: Set[str] = set()
169
+ processed_files: Set[str] = set()
184
170
 
185
- # Process source_code folder
186
- source_code_folder = get_folder_by_name(structure, "source_code")
187
- if source_code_folder:
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
- else:
197
- console.warning("No source_code folder found in remote project")
181
+ )
182
+ else:
183
+ console.warning("No source_code folder found in remote project")
198
184
 
199
- # Process evals folder
200
- evals_folder = get_folder_by_name(structure, "evals")
201
- if evals_folder:
202
- evals_path = os.path.join(root, "evals")
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
- else:
212
- console.warning("No evals folder found in remote project")
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)}")