11# type: ignore
22import json
33import os
4+ import re
45import subprocess
56import uuid
67import zipfile
78from string import Template
9+ from typing import Dict , Tuple
810
911import click
1012
@@ -50,6 +52,7 @@ def check_config(directory):
5052 "entryPoints" : config_data ["entryPoints" ],
5153 "version" : toml_data ["version" ],
5254 "authors" : toml_data ["authors" ],
55+ "dependencies" : toml_data .get ("dependencies" , {}),
5356 }
5457
5558
@@ -113,7 +116,7 @@ def handle_uv_operations(directory):
113116 run_uv_lock (directory )
114117
115118
116- def generate_operate_file (entryPoints ):
119+ def generate_operate_file (entryPoints , dependencies = None ):
117120 project_id = str (uuid .uuid4 ())
118121
119122 first_entry = entryPoints [0 ]
@@ -130,6 +133,10 @@ def generate_operate_file(entryPoints):
130133 "runtimeOptions" : {"requiresUserInteraction" : False , "isAttended" : False },
131134 }
132135
136+ # Add dependencies if provided
137+ if dependencies :
138+ operate_json_data ["dependencies" ] = dependencies
139+
133140 return operate_json_data
134141
135142
@@ -149,40 +156,6 @@ def generate_bindings_content():
149156 return bindings_content
150157
151158
152- def get_proposed_version (directory ):
153- output_dir = os .path .join (directory , ".uipath" )
154- if not os .path .exists (output_dir ):
155- return None
156-
157- # Get all .nupkg files
158- nupkg_files = [f for f in os .listdir (output_dir ) if f .endswith (".nupkg" )]
159- if not nupkg_files :
160- return None
161-
162- # Sort by modification time to get most recent
163- latest_file = max (
164- nupkg_files , key = lambda f : os .path .getmtime (os .path .join (output_dir , f ))
165- )
166-
167- # Extract version from filename
168- # Remove .nupkg extension first
169- name_version = latest_file [:- 6 ]
170- # Find 3rd last occurrence of . by splitting and joining parts
171- parts = name_version .split ("." )
172- if len (parts ) >= 3 :
173- version = "." .join (parts [- 3 :])
174- else :
175- version = name_version
176-
177- # Increment patch version by 1
178- try :
179- major , minor , patch = version .split ("." )
180- new_version = f"{ major } .{ minor } .{ int (patch ) + 1 } "
181- return new_version
182- except Exception :
183- return "0.0.1"
184-
185-
186159def generate_content_types_content ():
187160 templates_path = os .path .join (
188161 os .path .dirname (__file__ ), "_templates" , "[Content_Types].xml.template"
@@ -278,9 +251,10 @@ def pack_fn(
278251 version ,
279252 authors ,
280253 directory ,
254+ dependencies = None ,
281255 include_uv_lock = True ,
282256):
283- operate_file = generate_operate_file (entryPoints )
257+ operate_file = generate_operate_file (entryPoints , dependencies )
284258 entrypoints_file = generate_entrypoints_file (entryPoints )
285259
286260 # Get bindings from uipath.json if available
@@ -389,28 +363,166 @@ def pack_fn(
389363 z .writestr (f"content/{ file } " , f .read ())
390364
391365
392- def read_toml_project (file_path : str ) -> dict [str , any ]:
393- with open (file_path , "rb" ) as f :
394- content = tomllib .load (f )
395- if "project" not in content :
396- console .error ("pyproject.toml is missing the required field: project." )
397- if "name" not in content ["project" ]:
398- console .error ("pyproject.toml is missing the required field: project.name." )
399- if "description" not in content ["project" ]:
366+ def parse_dependency_string (dependency : str ) -> Tuple [str , str ]:
367+ """Parse a dependency string into package name and version specifier.
368+
369+ Handles PEP 508 dependency specifications including:
370+ - Simple names: "requests"
371+ - Version specifiers: "requests>=2.28.0"
372+ - Complex specifiers: "requests>=2.28.0,<3.0.0"
373+ - Extras: "requests[security]>=2.28.0"
374+ - Environment markers: "requests>=2.28.0; python_version>='3.8'"
375+
376+ Args:
377+ dependency: Raw dependency string from pyproject.toml
378+
379+ Returns:
380+ Tuple of (package_name, version_specifier)
381+
382+ Examples:
383+ "requests" -> ("requests", "*")
384+ "requests>=2.28.0" -> ("requests", ">=2.28.0")
385+ "requests>=2.28.0,<3.0.0" -> ("requests", ">=2.28.0,<3.0.0")
386+ "requests[security]>=2.28.0" -> ("requests", ">=2.28.0")
387+ """
388+ # Remove whitespace
389+ dependency = dependency .strip ()
390+
391+ # Handle environment markers (everything after semicolon)
392+ if ";" in dependency :
393+ dependency = dependency .split (";" )[0 ].strip ()
394+
395+ # Pattern to match package name with optional extras and version specifiers
396+ # Matches: package_name[extras] version_specs
397+ pattern = r"^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)(\[[^\]]+\])?(.*)"
398+ match = re .match (pattern , dependency )
399+
400+ if not match :
401+ # Fallback for edge cases
402+ return dependency , "*"
403+
404+ package_name = match .group (1 )
405+ version_part = match .group (4 ).strip () if match .group (4 ) else ""
406+
407+ # If no version specifier, return wildcard
408+ if not version_part :
409+ return package_name , "*"
410+
411+ # Clean up version specifier
412+ version_spec = version_part .strip ()
413+
414+ # Validate that version specifier starts with a valid operator
415+ valid_operators = [">=" , "<=" , "==" , "!=" , "~=" , ">" , "<" ]
416+ if not any (version_spec .startswith (op ) for op in valid_operators ):
417+ # If it doesn't start with an operator, treat as exact version
418+ if version_spec :
419+ version_spec = f"=={ version_spec } "
420+ else :
421+ version_spec = "*"
422+
423+ return package_name , version_spec
424+
425+
426+ def extract_dependencies_from_toml (project_data : Dict ) -> Dict [str , str ]:
427+ """Extract and parse dependencies from pyproject.toml project data.
428+
429+ Args:
430+ project_data: The "project" section from pyproject.toml
431+
432+ Returns:
433+ Dictionary mapping package names to version specifiers
434+ """
435+ dependencies = {}
436+
437+ if "dependencies" not in project_data :
438+ return dependencies
439+
440+ deps_list = project_data ["dependencies" ]
441+ if not isinstance (deps_list , list ):
442+ console .warning ("dependencies should be a list in pyproject.toml" )
443+ return dependencies
444+
445+ for dep in deps_list :
446+ if not isinstance (dep , str ):
447+ console .warning (f"Skipping non-string dependency: { dep } " )
448+ continue
449+
450+ try :
451+ name , version_spec = parse_dependency_string (dep )
452+ if name : # Only add if we got a valid name
453+ dependencies [name ] = version_spec
454+ except Exception as e :
455+ console .warning (f"Failed to parse dependency '{ dep } ': { e } " )
456+ continue
457+
458+ return dependencies
459+
460+
461+ def read_toml_project (file_path : str ) -> dict :
462+ """Read and parse pyproject.toml file with improved error handling and validation.
463+
464+ Args:
465+ file_path: Path to pyproject.toml file
466+
467+ Returns:
468+ Dictionary containing project metadata and dependencies
469+ """
470+ try :
471+ with open (file_path , "rb" ) as f :
472+ content = tomllib .load (f )
473+ except Exception as e :
474+ console .error (f"Failed to read or parse pyproject.toml: { e } " )
475+
476+ # Validate required sections
477+ if "project" not in content :
478+ console .error ("pyproject.toml is missing the required field: project." )
479+
480+ project = content ["project" ]
481+
482+ # Validate required fields with better error messages
483+ required_fields = {
484+ "name" : "Project name is required in pyproject.toml" ,
485+ "description" : "Project description is required in pyproject.toml" ,
486+ "version" : "Project version is required in pyproject.toml" ,
487+ }
488+
489+ for field , error_msg in required_fields .items ():
490+ if field not in project :
400491 console .error (
401- "pyproject.toml is missing the required field: project.description. "
492+ f "pyproject.toml is missing the required field: project.{ field } . { error_msg } "
402493 )
403- if "version" not in content ["project" ]:
494+
495+ # Check for empty values only if field exists
496+ if field in project and (
497+ not project [field ]
498+ or (isinstance (project [field ], str ) and not project [field ].strip ())
499+ ):
404500 console .error (
405- "pyproject.toml is missing the required field: project.version ."
501+ f"Project { field } cannot be empty. Please specify a { field } in pyproject.toml ."
406502 )
407503
408- return {
409- "name" : content ["project" ]["name" ],
410- "description" : content ["project" ]["description" ],
411- "version" : content ["project" ]["version" ],
412- "authors" : content ["project" ].get ("authors" , [{"name" : "" }])[0 ]["name" ],
413- }
504+ # Extract author information safely
505+ authors = project .get ("authors" , [])
506+ author_name = ""
507+
508+ if authors and isinstance (authors , list ) and len (authors ) > 0 :
509+ first_author = authors [0 ]
510+ if isinstance (first_author , dict ):
511+ author_name = first_author .get ("name" , "" )
512+ elif isinstance (first_author , str ):
513+ # Handle case where authors is a list of strings
514+ author_name = first_author
515+
516+ # Extract dependencies with improved parsing
517+ dependencies = extract_dependencies_from_toml (project )
518+
519+ return {
520+ "name" : project ["name" ].strip (),
521+ "description" : project ["description" ].strip (),
522+ "version" : project ["version" ].strip (),
523+ "authors" : author_name .strip (),
524+ "dependencies" : dependencies ,
525+ }
414526
415527
416528def get_project_version (directory ):
@@ -492,6 +604,7 @@ def pack(root, nolock):
492604 version or config ["version" ],
493605 config ["authors" ],
494606 root ,
607+ config .get ("dependencies" ),
495608 include_uv_lock = not nolock ,
496609 )
497610 display_project_info (config )
0 commit comments