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/cli_pack.py CHANGED
@@ -1,29 +1,37 @@
1
1
  # type: ignore
2
2
  import json
3
3
  import os
4
- import re
5
- import subprocess
6
4
  import uuid
7
5
  import zipfile
8
6
  from string import Template
9
- from typing import Dict, Tuple
10
7
 
11
8
  import click
12
9
 
13
- try:
14
- import tomllib
15
- except ImportError:
16
- import tomli as tomllib
17
-
18
10
  from ..telemetry import track
19
11
  from ._utils._console import ConsoleLogger
20
- from ._utils._constants import is_binary_file
12
+ from ._utils._project_files import (
13
+ ensure_config_file,
14
+ files_to_include,
15
+ get_project_config,
16
+ read_toml_project,
17
+ validate_config,
18
+ )
19
+ from ._utils._uv_helpers import handle_uv_operations
21
20
 
22
21
  console = ConsoleLogger()
23
22
 
24
23
  schema = "https://cloud.uipath.com/draft/2024-12/entry-point"
25
24
 
26
25
 
26
+ def get_project_version(directory):
27
+ toml_path = os.path.join(directory, "pyproject.toml")
28
+ if not os.path.exists(toml_path):
29
+ console.warning("pyproject.toml not found. Using default version 0.0.1")
30
+ return "0.0.1"
31
+ toml_data = read_toml_project(toml_path)
32
+ return toml_data["version"]
33
+
34
+
27
35
  def validate_config_structure(config_data):
28
36
  required_fields = ["entryPoints"]
29
37
  for field in required_fields:
@@ -31,92 +39,6 @@ def validate_config_structure(config_data):
31
39
  console.error(f"uipath.json is missing the required field: {field}.")
32
40
 
33
41
 
34
- def check_config(directory):
35
- config_path = os.path.join(directory, "uipath.json")
36
- toml_path = os.path.join(directory, "pyproject.toml")
37
-
38
- if not os.path.isfile(config_path):
39
- console.error("uipath.json not found, please run `uipath init`.")
40
- if not os.path.isfile(toml_path):
41
- console.error("pyproject.toml not found.")
42
-
43
- with open(config_path, "r") as config_file:
44
- config_data = json.load(config_file)
45
-
46
- validate_config_structure(config_data)
47
-
48
- toml_data = read_toml_project(toml_path)
49
-
50
- return {
51
- "project_name": toml_data["name"],
52
- "description": toml_data["description"],
53
- "entryPoints": config_data["entryPoints"],
54
- "version": toml_data["version"],
55
- "authors": toml_data["authors"],
56
- "dependencies": toml_data.get("dependencies", {}),
57
- }
58
-
59
-
60
- def is_uv_available():
61
- """Check if uv command is available in the system."""
62
- try:
63
- subprocess.run(["uv", "--version"], capture_output=True, check=True, timeout=20)
64
- return True
65
- except (subprocess.CalledProcessError, FileNotFoundError):
66
- return False
67
- except Exception as e:
68
- console.warning(
69
- f"An unexpected error occurred while checking uv availability: {str(e)}"
70
- )
71
- return False
72
-
73
-
74
- def is_uv_project(directory):
75
- """Check if this is a uv project by looking for the uv.lock file."""
76
- uv_lock_path = os.path.join(directory, "uv.lock")
77
-
78
- # If uv.lock exists, it's definitely a uv project
79
- if os.path.exists(uv_lock_path):
80
- return True
81
-
82
- return False
83
-
84
-
85
- def run_uv_lock(directory):
86
- """Run uv lock to update the lock file."""
87
- try:
88
- subprocess.run(
89
- ["uv", "lock"],
90
- cwd=directory,
91
- capture_output=True,
92
- text=True,
93
- check=True,
94
- timeout=60,
95
- )
96
- return True
97
- except subprocess.CalledProcessError as e:
98
- console.warning(f"uv lock failed: {e.stderr}")
99
- return False
100
- except FileNotFoundError:
101
- console.warning("uv command not found. Skipping lock file update.")
102
- return False
103
- except Exception as e:
104
- console.warning(f"An unexpected error occurred while running uv lock: {str(e)}")
105
- return False
106
-
107
-
108
- def handle_uv_operations(directory):
109
- """Handle uv operations if uv is detected and available."""
110
- if not is_uv_available():
111
- return
112
-
113
- if not is_uv_project(directory):
114
- return
115
-
116
- # Always run uv lock to ensure lock file is up to date
117
- run_uv_lock(directory)
118
-
119
-
120
42
  def generate_operate_file(entryPoints, dependencies=None):
121
43
  project_id = str(uuid.uuid4())
122
44
 
@@ -263,22 +185,12 @@ def pack_fn(
263
185
  if not os.path.exists(config_path):
264
186
  console.error("uipath.json not found, please run `uipath init`.")
265
187
 
266
- # Define the allowlist of file extensions to include
267
- file_extensions_included = [".py", ".mermaid", ".json", ".yaml", ".yml", ".md"]
268
- files_included = []
269
-
270
188
  with open(config_path, "r") as f:
271
189
  config_data = json.load(f)
272
190
  if "bindings" in config_data:
273
191
  bindings_content = config_data["bindings"]
274
192
  else:
275
193
  bindings_content = generate_bindings_content()
276
- if "settings" in config_data:
277
- settings = config_data["settings"]
278
- if "fileExtensionsIncluded" in settings:
279
- file_extensions_included.extend(settings["fileExtensionsIncluded"])
280
- if "filesIncluded" in settings:
281
- files_included = settings["filesIncluded"]
282
194
 
283
195
  content_types_content = generate_content_types_content()
284
196
  [psmdcp_file_name, psmdcp_content] = generate_psmdcp_content(
@@ -313,46 +225,31 @@ def pack_fn(
313
225
  z.writestr(f"{projectName}.nuspec", nuspec_content)
314
226
  z.writestr("_rels/.rels", rels_content)
315
227
 
316
- # Walk through directory and add all files with extensions in the allowlist
317
- for root, dirs, files in os.walk(directory):
318
- # Skip all directories that start with . or are a venv
319
- dirs[:] = [
320
- d
321
- for d in dirs
322
- if not d.startswith(".") and not is_venv_dir(os.path.join(root, d))
323
- ]
324
-
325
- for file in files:
326
- file_extension = os.path.splitext(file)[1].lower()
327
- if file_extension in file_extensions_included or file in files_included:
328
- file_path = os.path.join(root, file)
329
- rel_path = os.path.relpath(file_path, directory)
330
- if is_binary_file(file_extension):
331
- # Read binary files in binary mode
332
- with open(file_path, "rb") as f:
333
- z.writestr(f"content/{rel_path}", f.read())
334
- else:
335
- try:
336
- # Try UTF-8 first
337
- with open(file_path, "r", encoding="utf-8") as f:
338
- z.writestr(f"content/{rel_path}", f.read())
339
- except UnicodeDecodeError:
340
- # If UTF-8 fails, try with utf-8-sig (for files with BOM)
341
- try:
342
- with open(file_path, "r", encoding="utf-8-sig") as f:
343
- z.writestr(f"content/{rel_path}", f.read())
344
- except UnicodeDecodeError:
345
- # If that also fails, try with latin-1 as a fallback
346
- with open(file_path, "r", encoding="latin-1") as f:
347
- z.writestr(f"content/{rel_path}", f.read())
228
+ files = files_to_include(config_data, directory)
229
+
230
+ for file in files:
231
+ if file.is_binary:
232
+ # Read binary files in binary mode
233
+ with open(file.file_path, "rb") as f:
234
+ z.writestr(f"content/{file.relative_path}", f.read())
235
+ else:
236
+ try:
237
+ # Try UTF-8 first
238
+ with open(file.file_path, "r", encoding="utf-8") as f:
239
+ z.writestr(f"content/{file.relative_path}", f.read())
240
+ except UnicodeDecodeError:
241
+ # If UTF-8 fails, try with utf-8-sig (for files with BOM)
242
+ try:
243
+ with open(file.file_path, "r", encoding="utf-8-sig") as f:
244
+ z.writestr(f"content/{file.relative_path}", f.read())
245
+ except UnicodeDecodeError:
246
+ # If that also fails, try with latin-1 as a fallback
247
+ with open(file.file_path, "r", encoding="latin-1") as f:
248
+ z.writestr(f"content/{file.relative_path}", f.read())
348
249
 
349
250
  # Handle optional files, conditionally including uv.lock
350
- optional_files = ["pyproject.toml"]
351
251
  if include_uv_lock:
352
- optional_files.append("uv.lock")
353
-
354
- for file in optional_files:
355
- file_path = os.path.join(directory, file)
252
+ file_path = os.path.join(directory, "uv.lock")
356
253
  if os.path.exists(file_path):
357
254
  try:
358
255
  with open(file_path, "r", encoding="utf-8") as f:
@@ -362,177 +259,6 @@ def pack_fn(
362
259
  z.writestr(f"content/{file}", f.read())
363
260
 
364
261
 
365
- def parse_dependency_string(dependency: str) -> Tuple[str, str]:
366
- """Parse a dependency string into package name and version specifier.
367
-
368
- Handles PEP 508 dependency specifications including:
369
- - Simple names: "requests"
370
- - Version specifiers: "requests>=2.28.0"
371
- - Complex specifiers: "requests>=2.28.0,<3.0.0"
372
- - Extras: "requests[security]>=2.28.0"
373
- - Environment markers: "requests>=2.28.0; python_version>='3.8'"
374
-
375
- Args:
376
- dependency: Raw dependency string from pyproject.toml
377
-
378
- Returns:
379
- Tuple of (package_name, version_specifier)
380
-
381
- Examples:
382
- "requests" -> ("requests", "*")
383
- "requests>=2.28.0" -> ("requests", ">=2.28.0")
384
- "requests>=2.28.0,<3.0.0" -> ("requests", ">=2.28.0,<3.0.0")
385
- "requests[security]>=2.28.0" -> ("requests", ">=2.28.0")
386
- """
387
- # Remove whitespace
388
- dependency = dependency.strip()
389
-
390
- # Handle environment markers (everything after semicolon)
391
- if ";" in dependency:
392
- dependency = dependency.split(";")[0].strip()
393
-
394
- # Pattern to match package name with optional extras and version specifiers
395
- # Matches: package_name[extras] version_specs
396
- pattern = r"^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)(\[[^\]]+\])?(.*)"
397
- match = re.match(pattern, dependency)
398
-
399
- if not match:
400
- # Fallback for edge cases
401
- return dependency, "*"
402
-
403
- package_name = match.group(1)
404
- version_part = match.group(4).strip() if match.group(4) else ""
405
-
406
- # If no version specifier, return wildcard
407
- if not version_part:
408
- return package_name, "*"
409
-
410
- # Clean up version specifier
411
- version_spec = version_part.strip()
412
-
413
- # Validate that version specifier starts with a valid operator
414
- valid_operators = [">=", "<=", "==", "!=", "~=", ">", "<"]
415
- if not any(version_spec.startswith(op) for op in valid_operators):
416
- # If it doesn't start with an operator, treat as exact version
417
- if version_spec:
418
- version_spec = f"=={version_spec}"
419
- else:
420
- version_spec = "*"
421
-
422
- return package_name, version_spec
423
-
424
-
425
- def extract_dependencies_from_toml(project_data: Dict) -> Dict[str, str]:
426
- """Extract and parse dependencies from pyproject.toml project data.
427
-
428
- Args:
429
- project_data: The "project" section from pyproject.toml
430
-
431
- Returns:
432
- Dictionary mapping package names to version specifiers
433
- """
434
- dependencies = {}
435
-
436
- if "dependencies" not in project_data:
437
- return dependencies
438
-
439
- deps_list = project_data["dependencies"]
440
- if not isinstance(deps_list, list):
441
- console.warning("dependencies should be a list in pyproject.toml")
442
- return dependencies
443
-
444
- for dep in deps_list:
445
- if not isinstance(dep, str):
446
- console.warning(f"Skipping non-string dependency: {dep}")
447
- continue
448
-
449
- try:
450
- name, version_spec = parse_dependency_string(dep)
451
- if name: # Only add if we got a valid name
452
- dependencies[name] = version_spec
453
- except Exception as e:
454
- console.warning(f"Failed to parse dependency '{dep}': {e}")
455
- continue
456
-
457
- return dependencies
458
-
459
-
460
- def read_toml_project(file_path: str) -> dict:
461
- """Read and parse pyproject.toml file with improved error handling and validation.
462
-
463
- Args:
464
- file_path: Path to pyproject.toml file
465
-
466
- Returns:
467
- Dictionary containing project metadata and dependencies
468
- """
469
- try:
470
- with open(file_path, "rb") as f:
471
- content = tomllib.load(f)
472
- except Exception as e:
473
- console.error(f"Failed to read or parse pyproject.toml: {e}")
474
-
475
- # Validate required sections
476
- if "project" not in content:
477
- console.error("pyproject.toml is missing the required field: project.")
478
-
479
- project = content["project"]
480
-
481
- # Validate required fields with better error messages
482
- required_fields = {
483
- "name": "Project name is required in pyproject.toml",
484
- "description": "Project description is required in pyproject.toml",
485
- "version": "Project version is required in pyproject.toml",
486
- }
487
-
488
- for field, error_msg in required_fields.items():
489
- if field not in project:
490
- console.error(
491
- f"pyproject.toml is missing the required field: project.{field}. {error_msg}"
492
- )
493
-
494
- # Check for empty values only if field exists
495
- if field in project and (
496
- not project[field]
497
- or (isinstance(project[field], str) and not project[field].strip())
498
- ):
499
- console.error(
500
- f"Project {field} cannot be empty. Please specify a {field} in pyproject.toml."
501
- )
502
-
503
- # Extract author information safely
504
- authors = project.get("authors", [])
505
- author_name = ""
506
-
507
- if authors and isinstance(authors, list) and len(authors) > 0:
508
- first_author = authors[0]
509
- if isinstance(first_author, dict):
510
- author_name = first_author.get("name", "")
511
- elif isinstance(first_author, str):
512
- # Handle case where authors is a list of strings
513
- author_name = first_author
514
-
515
- # Extract dependencies with improved parsing
516
- dependencies = extract_dependencies_from_toml(project)
517
-
518
- return {
519
- "name": project["name"].strip(),
520
- "description": project["description"].strip(),
521
- "version": project["version"].strip(),
522
- "authors": author_name.strip(),
523
- "dependencies": dependencies,
524
- }
525
-
526
-
527
- def get_project_version(directory):
528
- toml_path = os.path.join(directory, "pyproject.toml")
529
- if not os.path.exists(toml_path):
530
- console.warning("pyproject.toml not found. Using default version 0.0.1")
531
- return "0.0.1"
532
- toml_data = read_toml_project(toml_path)
533
- return toml_data["version"]
534
-
535
-
536
262
  def display_project_info(config):
537
263
  max_label_length = max(
538
264
  len(label) for label in ["Name", "Version", "Description", "Authors"]
@@ -561,34 +287,9 @@ def pack(root, nolock):
561
287
  """Pack the project."""
562
288
  version = get_project_version(root)
563
289
 
564
- while not os.path.isfile(os.path.join(root, "uipath.json")):
565
- console.error(
566
- "uipath.json not found. Please run `uipath init` in the project directory."
567
- )
568
- config = check_config(root)
569
- if not config["project_name"] or config["project_name"].strip() == "":
570
- console.error(
571
- "Project name cannot be empty. Please specify a name in pyproject.toml."
572
- )
573
-
574
- if not config["description"] or config["description"].strip() == "":
575
- console.error(
576
- "Project description cannot be empty. Please specify a description in pyproject.toml."
577
- )
578
-
579
- if not config["authors"] or config["authors"].strip() == "":
580
- console.error(
581
- 'Project authors cannot be empty. Please specify authors in pyproject.toml:\n authors = [{ name = "John Doe" }]'
582
- )
583
-
584
- invalid_chars = ["&", "<", ">", '"', "'", ";"]
585
- for char in invalid_chars:
586
- if char in config["project_name"]:
587
- console.error(f"Project name contains invalid character: '{char}'")
588
-
589
- for char in invalid_chars:
590
- if char in config["description"]:
591
- console.error(f"Project description contains invalid character: '{char}'")
290
+ ensure_config_file(root)
291
+ config = get_project_config(root)
292
+ validate_config(config)
592
293
 
593
294
  with console.spinner("Packaging project ..."):
594
295
  try: