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/cli_push.py CHANGED
@@ -1,360 +1,41 @@
1
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
2
+ import asyncio
19
3
  import os
20
- from datetime import datetime, timezone
21
- from typing import Any, Dict, Optional, Set, Tuple
4
+ from typing import Any
22
5
  from urllib.parse import urlparse
23
6
 
24
7
  import click
25
- import httpx
26
- import jwt
27
8
  from dotenv import load_dotenv
28
9
 
29
- from .._utils._ssl_context import get_httpx_client_kwargs
30
10
  from ..telemetry import track
31
- from ._utils._common import get_env_vars
11
+ from ._push.sw_file_handler import SwFileHandler
32
12
  from ._utils._console import ConsoleLogger
33
13
  from ._utils._constants import (
34
- AGENT_INITIAL_CODE_VERSION,
35
- AGENT_STORAGE_VERSION,
36
- AGENT_TARGET_RUNTIME,
37
- AGENT_VERSION,
14
+ UIPATH_PROJECT_ID,
38
15
  )
39
16
  from ._utils._project_files import (
40
17
  ensure_config_file,
41
- files_to_include,
42
18
  get_project_config,
43
- read_toml_project,
44
19
  validate_config,
45
20
  )
46
- from ._utils._studio_project import ProjectFile, ProjectFolder, ProjectStructure
47
21
  from ._utils._uv_helpers import handle_uv_operations
48
22
 
49
23
  console = ConsoleLogger()
50
24
  load_dotenv(override=True)
51
25
 
52
26
 
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
27
  def get_org_scoped_url(base_url: str) -> str:
79
28
  parsed = urlparse(base_url)
80
29
  org_name, *_ = parsed.path.strip("/").split("/")
81
30
 
82
- # Construct the new scoped URL (scheme + domain + org_name)
83
31
  org_scoped_url = f"{parsed.scheme}://{parsed.netloc}/{org_name}"
84
32
  return org_scoped_url
85
33
 
86
34
 
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(
35
+ async def upload_source_files_to_project(
353
36
  project_id: str,
354
37
  config_data: dict[Any, str],
355
38
  directory: str,
356
- base_url: str,
357
- token: str,
358
39
  include_uv_lock: bool = True,
359
40
  ) -> None:
360
41
  """Upload source files to UiPath project.
@@ -365,130 +46,13 @@ def upload_source_files_to_project(
365
46
  - Deletes remote files that no longer exist locally
366
47
  - Optionally includes the UV lock file
367
48
  """
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
49
+ sw_file_handler = SwFileHandler(
50
+ project_id=project_id,
51
+ directory=directory,
52
+ include_uv_lock=include_uv_lock,
53
+ )
390
54
 
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
55
+ await sw_file_handler.upload_source_files(config_data)
492
56
 
493
57
 
494
58
  @click.command()
@@ -526,23 +90,21 @@ def push(root: str, nolock: bool) -> None:
526
90
  config = get_project_config(root)
527
91
  validate_config(config)
528
92
 
529
- if not os.getenv("UIPATH_PROJECT_ID", False):
93
+ if not os.getenv(UIPATH_PROJECT_ID, False):
530
94
  console.error("UIPATH_PROJECT_ID environment variable not found.")
531
- [base_url, token] = get_env_vars()
532
95
 
533
96
  with console.spinner("Pushing coded UiPath project to Studio Web..."):
534
97
  try:
535
- # Handle uv operations before packaging, unless nolock is specified
536
98
  if not nolock:
537
99
  handle_uv_operations(root)
538
100
 
539
- upload_source_files_to_project(
540
- os.getenv("UIPATH_PROJECT_ID"),
541
- config,
542
- root,
543
- base_url,
544
- token,
545
- include_uv_lock=not nolock,
101
+ asyncio.run(
102
+ upload_source_files_to_project(
103
+ os.getenv(UIPATH_PROJECT_ID),
104
+ config,
105
+ root,
106
+ include_uv_lock=not nolock,
107
+ )
546
108
  )
547
- except Exception:
548
- console.error("Failed to push UiPath project")
109
+ except Exception as e:
110
+ console.error(f"Failed to push UiPath project: {e}")
@@ -74,6 +74,7 @@ class BaseService:
74
74
  url: Union[URL, str],
75
75
  *,
76
76
  scoped: Literal["org", "tenant"] = "tenant",
77
+ infer_content_type=False,
77
78
  **kwargs: Any,
78
79
  ) -> Response:
79
80
  self._logger.debug(f"Request: {method} {url}")
@@ -105,6 +106,9 @@ class BaseService:
105
106
 
106
107
  scoped_url = self._url.scope_url(str(url), scoped)
107
108
 
109
+ if infer_content_type:
110
+ self._client_async.headers.pop("Content-Type", None)
111
+
108
112
  response = self._client.request(method, scoped_url, **kwargs)
109
113
 
110
114
  try:
@@ -128,6 +132,7 @@ class BaseService:
128
132
  url: Union[URL, str],
129
133
  *,
130
134
  scoped: Literal["org", "tenant"] = "tenant",
135
+ infer_content_type=False,
131
136
  **kwargs: Any,
132
137
  ) -> Response:
133
138
  self._logger.debug(f"Request: {method} {url}")
@@ -142,6 +147,9 @@ class BaseService:
142
147
 
143
148
  scoped_url = self._url.scope_url(str(url), scoped)
144
149
 
150
+ if infer_content_type:
151
+ self._client_async.headers.pop("Content-Type", None)
152
+
145
153
  response = await self._client_async.request(method, scoped_url, **kwargs)
146
154
 
147
155
  try:
@@ -25,6 +25,7 @@ class ApiClient(FolderContext, BaseService):
25
25
  method: str,
26
26
  url: Union[URL, str],
27
27
  scoped: Literal["org", "tenant"] = "tenant",
28
+ infer_content_type=False,
28
29
  **kwargs: Any,
29
30
  ) -> Response:
30
31
  if kwargs.get("include_folder_headers", False):
@@ -36,13 +37,16 @@ class ApiClient(FolderContext, BaseService):
36
37
  if "include_folder_headers" in kwargs:
37
38
  del kwargs["include_folder_headers"]
38
39
 
39
- return super().request(method, url, scoped=scoped, **kwargs)
40
+ return super().request(
41
+ method, url, scoped=scoped, infer_content_type=infer_content_type, **kwargs
42
+ )
40
43
 
41
44
  async def request_async(
42
45
  self,
43
46
  method: str,
44
47
  url: Union[URL, str],
45
48
  scoped: Literal["org", "tenant"] = "tenant",
49
+ infer_content_type=False,
46
50
  **kwargs: Any,
47
51
  ) -> Response:
48
52
  if kwargs.get("include_folder_headers", False):
@@ -54,4 +58,6 @@ class ApiClient(FolderContext, BaseService):
54
58
  if "include_folder_headers" in kwargs:
55
59
  del kwargs["include_folder_headers"]
56
60
 
57
- return await super().request_async(method, url, scoped=scoped, **kwargs)
61
+ return await super().request_async(
62
+ method, url, scoped=scoped, infer_content_type=infer_content_type, **kwargs
63
+ )
@@ -18,6 +18,7 @@ HEADER_USER_AGENT = "x-uipath-user-agent"
18
18
  HEADER_TENANT_ID = "x-uipath-tenantid"
19
19
  HEADER_INTERNAL_TENANT_ID = "x-uipath-internal-tenantid"
20
20
  HEADER_JOB_KEY = "x-uipath-jobkey"
21
+ HEADER_SW_LOCK_KEY = "x-uipath-sw-lockkey"
21
22
 
22
23
  # Data sources
23
24
  ORCHESTRATOR_STORAGE_BUCKET_DATA_SOURCE = (
@@ -18,18 +18,18 @@ class IngestionInProgressException(Exception):
18
18
  class EnrichedException(Exception):
19
19
  def __init__(self, error: HTTPStatusError) -> None:
20
20
  # Extract the relevant details from the HTTPStatusError
21
- status_code = error.response.status_code if error.response else "Unknown"
22
- url = str(error.request.url) if error.request else "Unknown"
23
- response_content = (
21
+ self.status_code = error.response.status_code if error.response else "Unknown"
22
+ self.url = str(error.request.url) if error.request else "Unknown"
23
+ self.response_content = (
24
24
  error.response.content.decode("utf-8")
25
25
  if error.response and error.response.content
26
26
  else "No content"
27
27
  )
28
28
 
29
29
  enriched_message = (
30
- f"\nRequest URL: {url}"
31
- f"\nStatus Code: {status_code}"
32
- f"\nResponse Content: {response_content}"
30
+ f"\nRequest URL: {self.url}"
31
+ f"\nStatus Code: {self.status_code}"
32
+ f"\nResponse Content: {self.response_content}"
33
33
  )
34
34
 
35
35
  # Initialize the parent Exception class with the formatted message
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uipath
3
- Version: 2.1.14
3
+ Version: 2.1.15
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