diff --git a/src/uipath/_cli/_auth/_auth_server.py b/src/uipath/_cli/_auth/_auth_server.py index f157bfad..fa673cf0 100644 --- a/src/uipath/_cli/_auth/_auth_server.py +++ b/src/uipath/_cli/_auth/_auth_server.py @@ -1,5 +1,6 @@ import http.server import json +import logging import os import socketserver import ssl @@ -9,6 +10,8 @@ from ._oidc_utils import get_auth_config +logger = logging.getLogger(__name__) + load_dotenv() # Server port @@ -38,7 +41,7 @@ def do_POST(self): content_length = int(self.headers["Content-Length"]) post_data = self.rfile.read(content_length) token_data = json.loads(post_data.decode("utf-8")) - print("Received authentication information") + logger.info("Received authentication information") self.send_response(200) self.end_headers() @@ -56,6 +59,7 @@ def do_POST(self): os.makedirs(uipath_dir, exist_ok=True) error_log_path = os.path.join(uipath_dir, ".error_log") + logger.debug(f"Writing error log to: {error_log_path}") with open(error_log_path, "a", encoding="utf-8") as f: f.write( f"\n--- Authentication Error Log {time.strftime('%Y-%m-%d %H:%M:%S')} ---\n" @@ -66,6 +70,7 @@ def do_POST(self): self.end_headers() self.wfile.write(b"Log received") else: + logger.warning(f"Received request to unknown path: {self.path}") self.send_error(404, "Path not found") def do_GET(self): @@ -73,6 +78,8 @@ def do_GET(self): # Always serve index.html regardless of the path try: index_path = os.path.join(os.path.dirname(__file__), "index.html") + logger.debug(f"Serving index.html from: {index_path}") + with open(index_path, "r") as f: content = f.read() @@ -93,7 +100,9 @@ def do_GET(self): self.send_header("Content-Length", str(len(content))) self.end_headers() self.wfile.write(content.encode("utf-8")) + logger.debug("Successfully served index.html") except FileNotFoundError: + logger.error(f"Index file not found at: {index_path}") self.send_error(404, "File not found") def end_headers(self): @@ -106,8 +115,6 @@ def do_OPTIONS(self): self.send_response(200) self.end_headers() - return SimpleHTTPSRequestHandler - class HTTPSServer: def __init__(self, port=6234, cert_file="localhost.crt", key_file="localhost.key"): @@ -125,6 +132,7 @@ def __init__(self, port=6234, cert_file="localhost.crt", key_file="localhost.key self.httpd = None self.token_data = None self.should_shutdown = False + logger.debug(f"Initialized HTTPS server on port {port}") def token_received_callback(self, token_data): """Callback for when a token is received. @@ -134,6 +142,7 @@ def token_received_callback(self, token_data): """ self.token_data = token_data self.should_shutdown = True + logger.debug("Token received callback triggered") def create_server(self, state, code_verifier, domain): """Create and configure the HTTPS server. @@ -146,6 +155,7 @@ def create_server(self, state, code_verifier, domain): Returns: socketserver.TCPServer: The configured HTTPS server. """ + logger.debug("Creating SSL context") # Create SSL context context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain(self.cert_file, self.key_file) @@ -157,6 +167,7 @@ def create_server(self, state, code_verifier, domain): ) self.httpd = socketserver.TCPServer(("", self.port), handler) self.httpd.socket = context.wrap_socket(self.httpd.socket, server_side=True) + logger.debug("Server created successfully") return self.httpd @@ -176,10 +187,11 @@ def start(self, state, code_verifier, domain): try: if self.httpd: + logger.info(f"Starting server on port {self.port}") while not self.should_shutdown: self.httpd.handle_request() except KeyboardInterrupt: - print("Process interrupted by user") + logger.info("Process interrupted by user") finally: self.stop() @@ -188,5 +200,7 @@ def start(self, state, code_verifier, domain): def stop(self): """Stop the server gracefully and cleanup resources.""" if self.httpd: + logger.debug("Stopping server") self.httpd.server_close() self.httpd = None + logger.debug("Server stopped") diff --git a/src/uipath/_cli/cli_auth.py b/src/uipath/_cli/cli_auth.py index 8793afc5..2114f812 100644 --- a/src/uipath/_cli/cli_auth.py +++ b/src/uipath/_cli/cli_auth.py @@ -1,5 +1,6 @@ # type: ignore import json +import logging import os import socket import webbrowser @@ -7,12 +8,15 @@ import click from dotenv import load_dotenv +from .._utils._logs import setup_logging from ._auth._auth_server import HTTPSServer from ._auth._oidc_utils import get_auth_config, get_auth_url from ._auth._portal_service import PortalService, select_tenant from ._auth._utils import update_auth_file, update_env_file from ._utils._common import environment_options +logger = logging.getLogger(__name__) + load_dotenv() @@ -32,66 +36,100 @@ def set_port(): port_option_one = auth_config.get("portOptionOne", 8104) port_option_two = auth_config.get("portOptionTwo", 8055) port_option_three = auth_config.get("portOptionThree", 42042) + + logger.debug(f"Checking port availability. Initial port: {port}") + if is_port_in_use(port): + logger.debug(f"Port {port} is in use, trying alternatives") if is_port_in_use(port_option_one): if is_port_in_use(port_option_two): if is_port_in_use(port_option_three): + logger.error("All configured ports are in use") raise RuntimeError( "All configured ports are in use. Please close applications using ports or configure different ports." ) else: port = port_option_three + logger.debug(f"Using port option three: {port}") else: port = port_option_two + logger.debug(f"Using port option two: {port}") else: port = port_option_one + logger.debug(f"Using port option one: {port}") + else: + logger.debug(f"Using initial port: {port}") + auth_config["port"] = port - with open( - os.path.join(os.path.dirname(__file__), "..", "auth_config.json"), "w" - ) as f: + config_path = os.path.join(os.path.dirname(__file__), "..", "auth_config.json") + logger.debug(f"Updating auth config at: {config_path}") + with open(config_path, "w") as f: json.dump(auth_config, f) @click.command() @environment_options -def auth(domain="alpha"): +@click.option( + "--verbose", + "-v", + is_flag=True, + help="Enable verbose logging", +) +def auth(domain="alpha", verbose=False): """Authenticate with UiPath Cloud Platform.""" + # Setup logging based on verbose flag + setup_logging(should_debug=verbose) + + logger.debug(f"Starting authentication process for domain: {domain}") portal_service = PortalService(domain) + if ( os.getenv("UIPATH_URL") and os.getenv("UIPATH_TENANT_ID") and os.getenv("UIPATH_ORGANIZATION_ID") ): + logger.debug("Checking existing authentication") try: portal_service.ensure_valid_token() - click.echo("Authentication successful") + logger.info("Authentication successful") return - except Exception: - click.echo( + except Exception as e: + logger.warning(f"Existing authentication invalid: {str(e)}") + logger.info( "Authentication not found or expired. Please authenticate again." ) + logger.debug("Generating auth URL") auth_url, code_verifier, state = get_auth_url(domain) + logger.debug("Opening browser for authentication") webbrowser.open(auth_url, 1) auth_config = get_auth_config() - print( + logger.info( "If a browser window did not open, please open the following URL in your browser:" ) - print(auth_url) + logger.info(auth_url) + + logger.debug("Starting auth server") server = HTTPSServer(port=auth_config["port"]) token_data = server.start(state, code_verifier, domain) + try: if token_data: + logger.debug("Token received, updating services") portal_service.update_token_data(token_data) update_auth_file(token_data) access_token = token_data["access_token"] update_env_file({"UIPATH_ACCESS_TOKEN": access_token}) + logger.debug("Fetching tenants and organizations") tenants_and_organizations = portal_service.get_tenants_and_organizations() select_tenant(domain, tenants_and_organizations) + logger.info("Authentication completed successfully") else: + logger.error("No token data received") click.echo("Authentication failed") except Exception as e: + logger.error(f"Authentication failed: {str(e)}") click.echo(f"Authentication failed: {e}") diff --git a/src/uipath/_cli/cli_init.py b/src/uipath/_cli/cli_init.py index e7c111d1..db958ca1 100644 --- a/src/uipath/_cli/cli_init.py +++ b/src/uipath/_cli/cli_init.py @@ -1,5 +1,6 @@ # type: ignore import json +import logging import os import traceback import uuid @@ -8,107 +9,137 @@ import click +from .._utils._logs import setup_logging from ._utils._input_args import generate_args from ._utils._parse_ast import generate_bindings_json from .middlewares import Middlewares +logger = logging.getLogger(__name__) + def generate_env_file(target_directory): env_path = os.path.join(target_directory, ".env") + logger.debug(f"Checking for .env file at: {env_path}") if not os.path.exists(env_path): relative_path = os.path.relpath(env_path, target_directory) - click.echo(f"Created {relative_path} file.") + logger.info(f"Creating {relative_path} file") with open(env_path, "w") as f: f.write("UIPATH_ACCESS_TOKEN=YOUR_TOKEN_HERE\n") f.write("UIPATH_URL=https://cloud.uipath.com/ACCOUNT_NAME/TENANT_NAME\n") + logger.debug("Created .env file with default values") def get_user_script(directory: str, entrypoint: Optional[str] = None) -> Optional[str]: """Find the Python script to process.""" + logger.debug(f"Looking for script in directory: {directory}") + if entrypoint: script_path = os.path.join(directory, entrypoint) + logger.debug(f"Checking specified entrypoint: {script_path}") if not os.path.isfile(script_path): - click.echo(f"The {entrypoint} file does not exist in the current directory") + logger.error( + f"The {entrypoint} file does not exist in the current directory" + ) return None return script_path python_files = [f for f in os.listdir(directory) if f.endswith(".py")] + logger.debug(f"Found Python files: {python_files}") if not python_files: - click.echo("No Python files found in the directory") + logger.error("No Python files found in the directory") return None elif len(python_files) == 1: - return os.path.join(directory, python_files[0]) + script_path = os.path.join(directory, python_files[0]) + logger.debug(f"Using single Python file found: {script_path}") + return script_path else: - click.echo( - "Multiple Python files found in the current directory.\nPlease specify the entrypoint: `uipath init `" - ) + logger.warning("Multiple Python files found in the current directory") + logger.info("Please specify the entrypoint: `uipath init `") return None @click.command() @click.argument("entrypoint", required=False, default=None) -def init(entrypoint: str) -> None: +@click.option( + "--verbose", + "-v", + is_flag=True, + help="Enable verbose logging", +) +def init(entrypoint: str, verbose: bool) -> None: """Initialize a uipath.json configuration file for the script.""" + # Setup logging based on verbose flag + setup_logging(should_debug=verbose) + + logger.debug(f"Starting init command with entrypoint: {entrypoint}") current_directory = os.getcwd() + logger.debug(f"Current working directory: {current_directory}") + generate_env_file(current_directory) + logger.debug("Running init middlewares") result = Middlewares.next("init", entrypoint) if result.error_message: - click.echo(result.error_message) + logger.error(result.error_message) if result.should_include_stacktrace: - click.echo(traceback.format_exc()) + logger.error(traceback.format_exc()) click.get_current_context().exit(1) if result.info_message: - click.echo(result.info_message) + logger.info(result.info_message) if not result.should_continue: + logger.debug("Middleware chain stopped execution") return script_path = get_user_script(current_directory, entrypoint=entrypoint) if not script_path: + logger.error("No valid script found") click.get_current_context().exit(1) try: + logger.debug(f"Generating args for script: {script_path}") args = generate_args(script_path) relative_path = Path(script_path).relative_to(current_directory).as_posix() + logger.debug(f"Relative path to script: {relative_path}") config_data = { "entryPoints": [ { "filePath": relative_path, "uniqueId": str(uuid.uuid4()), - # "type": "process", OR BE doesn't offer json schema support for type: Process "type": "agent", "input": args["input"], "output": args["output"], } ] } + logger.debug("Created base config data") # Generate bindings JSON based on the script path try: + logger.debug("Generating bindings JSON") bindings_data = generate_bindings_json(script_path) # Add bindings to the config data config_data["bindings"] = bindings_data - - click.echo("Bindings generated successfully.") + logger.info("Bindings generated successfully") except Exception as e: - click.echo(f"Warning: Could not generate bindings: {str(e)}") + logger.warning(f"Could not generate bindings: {str(e)}") config_path = "uipath.json" + logger.debug(f"Writing config to: {config_path}") with open(config_path, "w") as config_file: json.dump(config_data, config_file, indent=4) - click.echo(f"Configuration file {config_path} created successfully.") + logger.info(f"Configuration file {config_path} created successfully") except Exception as e: - click.echo(f"Error generating configuration: {str(e)}") - click.echo(traceback.format_exc()) + logger.error(f"Error generating configuration: {str(e)}") + logger.error(traceback.format_exc()) click.get_current_context().exit(1) diff --git a/src/uipath/_cli/cli_pack.py b/src/uipath/_cli/cli_pack.py index ffb73d3f..e23174eb 100644 --- a/src/uipath/_cli/cli_pack.py +++ b/src/uipath/_cli/cli_pack.py @@ -1,5 +1,6 @@ # type: ignore import json +import logging import os import uuid import zipfile @@ -12,6 +13,10 @@ except ImportError: import tomli as tomllib +from .._utils._logs import setup_logging + +logger = logging.getLogger(__name__) + schema = "https://cloud.uipath.com/draft/2024-12/entry-point" @@ -19,6 +24,7 @@ def validate_config_structure(config_data): required_fields = ["entryPoints"] for field in required_fields: if field not in config_data: + logger.error(f"uipath.json is missing the required field: {field}") raise Exception(f"uipath.json is missing the required field: {field}") @@ -27,15 +33,19 @@ def check_config(directory): toml_path = os.path.join(directory, "pyproject.toml") if not os.path.isfile(config_path): + logger.error("uipath.json not found, please run `uipath init`") raise Exception("uipath.json not found, please run `uipath init`") if not os.path.isfile(toml_path): + logger.error("pyproject.toml not found") raise Exception("pyproject.toml not found") + logger.debug(f"Reading config from: {config_path}") with open(config_path, "r") as config_file: config_data = json.load(config_file) validate_config_structure(config_data) + logger.debug(f"Reading TOML from: {toml_path}") toml_data = read_toml_project(toml_path) return { @@ -49,10 +59,12 @@ def check_config(directory): def generate_operate_file(entryPoints): project_id = str(uuid.uuid4()) + logger.debug(f"Generated project ID: {project_id}") first_entry = entryPoints[0] file_path = first_entry["filePath"] type = first_entry["type"] + logger.debug(f"Using first entry point: {file_path} of type {type}") operate_json_data = { "$schema": schema, @@ -68,6 +80,7 @@ def generate_operate_file(entryPoints): def generate_entrypoints_file(entryPoints): + logger.debug("Generating entrypoints file") entrypoint_json_data = { "$schema": schema, "$id": "entry-points.json", @@ -78,6 +91,7 @@ def generate_entrypoints_file(entryPoints): def generate_bindings_content(): + logger.debug("Generating empty bindings content") bindings_content = {"version": "2.0", "resources": []} return bindings_content @@ -86,17 +100,20 @@ def generate_bindings_content(): def get_proposed_version(directory): output_dir = os.path.join(directory, ".uipath") if not os.path.exists(output_dir): + logger.debug("No .uipath directory found, no version to propose") return None # Get all .nupkg files nupkg_files = [f for f in os.listdir(output_dir) if f.endswith(".nupkg")] if not nupkg_files: + logger.debug("No .nupkg files found, no version to propose") return None # Sort by modification time to get most recent latest_file = max( nupkg_files, key=lambda f: os.path.getmtime(os.path.join(output_dir, f)) ) + logger.debug(f"Found latest package: {latest_file}") # Extract version from filename # Remove .nupkg extension first @@ -112,8 +129,10 @@ def get_proposed_version(directory): try: major, minor, patch = version.split(".") new_version = f"{major}.{minor}.{int(patch) + 1}" + logger.debug(f"Proposing new version: {new_version}") return new_version except Exception: + logger.debug("Failed to parse version, returning default 0.0.1") return "0.0.1" @@ -121,6 +140,7 @@ def generate_content_types_content(): templates_path = os.path.join( os.path.dirname(__file__), "_templates", "[Content_Types].xml.template" ) + logger.debug(f"Reading content types template from: {templates_path}") with open(templates_path, "r") as file: content_types_content = file.read() return content_types_content @@ -136,18 +156,20 @@ def generate_nuspec_content(projectName, packageVersion, description, authors): templates_path = os.path.join( os.path.dirname(__file__), "_templates", "package.nuspec.template" ) + logger.debug(f"Reading nuspec template from: {templates_path}") with open(templates_path, "r", encoding="utf-8-sig") as f: content = f.read() return Template(content).substitute(variables) def generate_rels_content(nuspecPath, psmdcpPath): - # /package/services/metadata/core-properties/254324ccede240e093a925f0231429a0.psmdcp templates_path = os.path.join( os.path.dirname(__file__), "_templates", ".rels.template" ) nuspecId = "R" + str(uuid.uuid4()).replace("-", "")[:16] psmdcpId = "R" + str(uuid.uuid4()).replace("-", "")[:16] + logger.debug(f"Generated IDs - nuspec: {nuspecId}, psmdcp: {psmdcpId}") + variables = { "nuspecPath": nuspecPath, "nuspecId": nuspecId, @@ -166,6 +188,8 @@ def generate_psmdcp_content(projectName, version, description, authors): token = str(uuid.uuid4()).replace("-", "")[:32] random_file_name = f"{uuid.uuid4().hex[:16]}.psmdcp" + logger.debug(f"Generated psmdcp file name: {random_file_name}") + variables = { "creator": authors, "description": description, @@ -180,6 +204,7 @@ def generate_psmdcp_content(projectName, version, description, authors): def generate_package_descriptor_content(entryPoints): + logger.debug("Generating package descriptor content") files = { "operate.json": "content/operate.json", "entry-points.json": "content/entry-points.json", @@ -198,30 +223,39 @@ def generate_package_descriptor_content(entryPoints): def pack_fn(projectName, description, entryPoints, version, authors, directory): + logger.info("Starting package generation") operate_file = generate_operate_file(entryPoints) entrypoints_file = generate_entrypoints_file(entryPoints) # Get bindings from uipath.json if available config_path = os.path.join(directory, "uipath.json") if not os.path.exists(config_path): + logger.error("uipath.json not found, please run `uipath init`") raise Exception("uipath.json not found, please run `uipath init`") # Define the allowlist of file extensions to include file_extensions_included = [".py", ".mermaid", ".json", ".yaml", ".yml"] files_included = [] + logger.debug(f"Reading config from: {config_path}") with open(config_path, "r") as f: config_data = json.load(f) if "bindings" in config_data: + logger.debug("Using bindings from config") bindings_content = config_data["bindings"] else: + logger.debug("Generating empty bindings") bindings_content = generate_bindings_content() if "settings" in config_data: settings = config_data["settings"] if "fileExtensionsIncluded" in settings: file_extensions_included.extend(settings["fileExtensionsIncluded"]) + logger.debug( + f"Added custom file extensions: {settings['fileExtensionsIncluded']}" + ) if "filesIncluded" in settings: files_included = settings["filesIncluded"] + logger.debug(f"Added custom files: {files_included}") content_types_content = generate_content_types_content() [psmdcp_file_name, psmdcp_content] = generate_psmdcp_content( @@ -236,11 +270,14 @@ def pack_fn(projectName, description, entryPoints, version, authors, directory): # Create .uipath directory if it doesn't exist os.makedirs(".uipath", exist_ok=True) + logger.debug("Created .uipath directory") - with zipfile.ZipFile( - f".uipath/{projectName}.{version}.nupkg", "w", zipfile.ZIP_DEFLATED - ) as z: + package_path = f".uipath/{projectName}.{version}.nupkg" + logger.info(f"Creating package at: {package_path}") + + with zipfile.ZipFile(package_path, "w", zipfile.ZIP_DEFLATED) as z: # Add metadata files + logger.debug("Adding metadata files to package") z.writestr( f"./package/services/metadata/core-properties/{psmdcp_file_name}", psmdcp_content, @@ -257,6 +294,7 @@ def pack_fn(projectName, description, entryPoints, version, authors, directory): z.writestr("_rels/.rels", rels_content) # Walk through directory and add all files with extensions in the allowlist + logger.debug("Adding project files to package") for root, dirs, files in os.walk(directory): # Skip all directories that start with . dirs[:] = [d for d in dirs if not d.startswith(".")] @@ -266,6 +304,7 @@ def pack_fn(projectName, description, entryPoints, version, authors, directory): if file_extension in file_extensions_included or file in files_included: file_path = os.path.join(root, file) rel_path = os.path.relpath(file_path, directory) + logger.debug(f"Adding file: {rel_path}") try: # Try UTF-8 first with open(file_path, "r", encoding="utf-8") as f: @@ -284,6 +323,7 @@ def pack_fn(projectName, description, entryPoints, version, authors, directory): for file in optional_files: file_path = os.path.join(directory, file) if os.path.exists(file_path): + logger.debug(f"Adding optional file: {file}") try: with open(file_path, "r", encoding="utf-8") as f: z.writestr(f"content/{file}", f.read()) @@ -291,21 +331,32 @@ def pack_fn(projectName, description, entryPoints, version, authors, directory): with open(file_path, "r", encoding="latin-1") as f: z.writestr(f"content/{file}", f.read()) + logger.info("Package generation completed successfully") + def read_toml_project(file_path: str) -> dict[str, any]: + logger.debug(f"Reading TOML project from: {file_path}") with open(file_path, "rb") as f: content = tomllib.load(f) if "project" not in content: + logger.error("pyproject.toml is missing the required field: project") raise Exception("pyproject.toml is missing the required field: project") if "name" not in content["project"]: + logger.error("pyproject.toml is missing the required field: project.name") raise Exception( "pyproject.toml is missing the required field: project.name" ) if "description" not in content["project"]: + logger.error( + "pyproject.toml is missing the required field: project.description" + ) raise Exception( "pyproject.toml is missing the required field: project.description" ) if "version" not in content["project"]: + logger.error( + "pyproject.toml is missing the required field: project.version" + ) raise Exception( "pyproject.toml is missing the required field: project.version" ) @@ -321,7 +372,7 @@ def read_toml_project(file_path: str) -> dict[str, any]: def get_project_version(directory): toml_path = os.path.join(directory, "pyproject.toml") if not os.path.exists(toml_path): - click.echo("Warning: No pyproject.toml found. Using default version 0.0.1") + logger.warning("No pyproject.toml found. Using default version 0.0.1") return "0.0.1" toml_data = read_toml_project(toml_path) return toml_data["version"] @@ -329,19 +380,32 @@ def get_project_version(directory): @click.command() @click.argument("root", type=str, default="./") -def pack(root): +@click.option( + "--verbose", + "-v", + is_flag=True, + help="Enable verbose logging", +) +def pack(root, verbose): + # Setup logging based on verbose flag + setup_logging(should_debug=verbose) + + logger.debug(f"Starting pack command with root: {root}") version = get_project_version(root) while not os.path.isfile(os.path.join(root, "uipath.json")): - click.echo( + logger.error( "uipath.json not found. Please run `uipath init` in the project directory." ) return + config = check_config(root) if not config["project_name"] or config["project_name"].strip() == "": + logger.error("Project name cannot be empty") raise Exception("Project name cannot be empty") if not config["description"] or config["description"].strip() == "": + logger.error("Project description cannot be empty") raise Exception("Project description cannot be empty") if not config["authors"] or config["authors"].strip() == "": @@ -350,14 +414,18 @@ def pack(root): invalid_chars = ["&", "<", ">", '"', "'", ";"] for char in invalid_chars: if char in config["project_name"]: + logger.error(f"Project name contains invalid character: '{char}'") raise Exception(f"Project name contains invalid character: '{char}'") for char in invalid_chars: if char in config["description"]: + logger.error(f"Project description contains invalid character: '{char}'") raise Exception(f"Project description contains invalid character: '{char}'") - click.echo( + + logger.info( f"Packaging project {config['project_name']}:{version or config['version']} description {config['description']} authored by {config['authors']}" ) + pack_fn( config["project_name"], config["description"], diff --git a/src/uipath/_cli/cli_publish.py b/src/uipath/_cli/cli_publish.py index e8eeab9c..ddbf00b7 100644 --- a/src/uipath/_cli/cli_publish.py +++ b/src/uipath/_cli/cli_publish.py @@ -1,15 +1,21 @@ # type: ignore +import json +import logging import os import click import requests from dotenv import load_dotenv +from .._utils._logs import setup_logging + +logger = logging.getLogger(__name__) + def get_most_recent_package(): nupkg_files = [f for f in os.listdir(".uipath") if f.endswith(".nupkg")] if not nupkg_files: - click.echo("No .nupkg file found in .uipath directory") + logger.warning("No .nupkg file found in .uipath directory") return # Get full path and modification time for each file @@ -29,10 +35,10 @@ def get_env_vars(): token = os.environ.get("UIPATH_ACCESS_TOKEN") if not all([base_url, token]): - click.echo( + logger.error( "Missing required environment variables. Please check your .env file contains:" ) - click.echo("UIPATH_URL, UIPATH_ACCESS_TOKEN") + logger.error("UIPATH_URL, UIPATH_ACCESS_TOKEN") raise click.Abort("Missing environment variables") return [base_url, token] @@ -53,61 +59,95 @@ def get_env_vars(): flag_value="personal", help="Whether to publish to the personal workspace", ) -def publish(feed): +@click.option( + "--verbose", + "-v", + is_flag=True, + help="Enable verbose logging", +) +def publish(feed, verbose): + current_path = os.getcwd() + load_dotenv(os.path.join(current_path, ".env"), override=True) + # Setup logging based on verbose flag + setup_logging(should_debug=verbose) + current_path = os.getcwd() + logger.debug(f"Current working directory: {current_path}") + load_dotenv(os.path.join(current_path, ".env"), override=True) + logger.debug("Loaded environment variables from .env file") + if feed is None: click.echo("Select feed type:") click.echo(" 0: Tenant package feed") click.echo(" 1: Personal workspace") feed_idx = click.prompt("Select feed", type=int) feed = "tenant" if feed_idx == 0 else "personal" - click.echo(f"Selected feed: {feed}") + logger.info(f"Selected feed: {feed}") + os.makedirs(".uipath", exist_ok=True) + logger.debug("Ensured .uipath directory exists") # Find most recent .nupkg file in .uipath directory most_recent = get_most_recent_package() if not most_recent: - click.echo("Error: No package files found in .uipath directory") + logger.error("No package files found in .uipath directory") raise click.Abort() - click.echo(f"Publishing most recent package: {most_recent}") + logger.info(f"Publishing most recent package: {most_recent}") package_to_publish_path = os.path.join(".uipath", most_recent) [base_url, token] = get_env_vars() + logger.debug(f"Using base URL: {base_url}") url = f"{base_url}/orchestrator_/odata/Processes/UiPath.Server.Configuration.OData.UploadPackage()" if feed == "personal": + logger.debug("Using personal workspace feed") # Get current user extended info to get personal workspace ID user_url = f"{base_url}/orchestrator_/odata/Users/UiPath.Server.Configuration.OData.GetCurrentUserExtended" + logger.debug(f"Fetching user info from: {user_url}") + user_response = requests.get( user_url, headers={"Authorization": f"Bearer {token}"} ) if user_response.status_code != 200: - click.echo("Failed to get user info") - click.echo(f"Response: {user_response.text}") + logger.error( + f"Failed to get user info. Status code: {user_response.status_code}" + ) + logger.error(f"Response: {user_response.text}") raise click.Abort() - user_data = user_response.json() + user_data = {} + try: + user_data = user_response.json() + logger.debug("Successfully retrieved user data") + except json.JSONDecodeError as e: + logger.error("Failed to decode UserExtendedInfo response") + raise click.Abort() from e + personal_workspace_id = user_data.get("PersonalWorskpaceFeedId") if not personal_workspace_id: - click.echo("No personal workspace found for user") + logger.error("No personal workspace found for user") raise click.Abort() url = url + "?feedId=" + personal_workspace_id + logger.debug(f"Updated URL with personal workspace ID: {personal_workspace_id}") headers = {"Authorization": f"Bearer {token}"} + logger.debug("Prepared request headers") + logger.info("Uploading package...") with open(package_to_publish_path, "rb") as f: files = {"file": (package_to_publish_path, f, "application/octet-stream")} response = requests.post(url, headers=headers, files=files) if response.status_code == 200: - click.echo("Package published successfully!") + logger.info("Package published successfully!") else: - click.echo(f"Failed to publish package. Status code: {response.status_code}") - click.echo(f"Response: {response.text}") + logger.error(f"Failed to publish package. Status code: {response.status_code}") + logger.error(f"Response: {response.text}") + raise click.Abort("Failed to publish package") diff --git a/src/uipath/_cli/cli_run.py b/src/uipath/_cli/cli_run.py index 4d1c7b8a..4801733c 100644 --- a/src/uipath/_cli/cli_run.py +++ b/src/uipath/_cli/cli_run.py @@ -10,6 +10,7 @@ import click from dotenv import load_dotenv +from .._utils._logs import setup_logging from ._runtime._contracts import ( UiPathRuntimeContext, UiPathRuntimeError, @@ -19,7 +20,6 @@ from .middlewares import MiddlewareResult, Middlewares logger = logging.getLogger(__name__) -load_dotenv() def python_run_middleware( @@ -36,6 +36,7 @@ def python_run_middleware( MiddlewareResult with execution status and messages """ if not entrypoint: + logger.error("No entrypoint specified") return MiddlewareResult( should_continue=False, info_message="""Error: No entrypoint specified. Please provide a path to a Python script. @@ -43,6 +44,7 @@ def python_run_middleware( ) if not os.path.exists(entrypoint): + logger.error(f"Script not found at path {entrypoint}") return MiddlewareResult( should_continue=False, error_message=f"""Error: Script not found at path {entrypoint}. @@ -50,11 +52,15 @@ def python_run_middleware( ) try: + logger.debug(f"Starting execution of {entrypoint}") + logger.debug(f"Input data: {input}") + logger.debug(f"Resume flag: {resume}") async def execute(): - context = UiPathRuntimeContext.from_config( - env.get("UIPATH_CONFIG_PATH", "uipath.json") - ) + config_path = env.get("UIPATH_CONFIG_PATH", "uipath.json") + logger.debug(f"Loading config from: {config_path}") + context = UiPathRuntimeContext.from_config(config_path) + context.entrypoint = entrypoint context.input = input context.resume = resume @@ -74,23 +80,25 @@ async def execute(): reference_id=env.get("UIPATH_JOB_KEY") or str(uuid4()), ) context.logs_min_level = env.get("LOG_LEVEL", "INFO") + logger.debug(f"Runtime context: {context}") async with UiPathRuntime.from_context(context) as runtime: + logger.info("Starting runtime execution") await runtime.execute() + logger.info("Runtime execution completed") asyncio.run(execute()) - - # Return success + logger.info("Execution completed successfully") return MiddlewareResult(should_continue=False) except UiPathRuntimeError as e: + logger.error(f"Runtime error: {e.error_info.title} - {e.error_info.detail}") return MiddlewareResult( should_continue=False, error_message=f"Error: {e.error_info.title} - {e.error_info.detail}", should_include_stacktrace=False, ) except Exception as e: - # Handle unexpected errors logger.exception("Unexpected error in Python runtime middleware") return MiddlewareResult( should_continue=False, @@ -103,28 +111,51 @@ async def execute(): @click.argument("entrypoint", required=False) @click.argument("input", required=False, default="{}") @click.option("--resume", is_flag=True, help="Resume execution from a previous state") -def run(entrypoint: Optional[str], input: Optional[str], resume: bool) -> None: +@click.option( + "--verbose", + "-v", + is_flag=True, + help="Enable verbose logging", +) +def run( + entrypoint: Optional[str], input: Optional[str], resume: bool, verbose: bool +) -> None: """Execute a Python script with JSON input.""" + # Setup logging based on verbose flag + setup_logging(should_debug=verbose) + + # Load environment variables with override + current_path = os.getcwd() + logger.debug(f"Loading environment variables from {current_path}/.env") + load_dotenv(os.path.join(current_path, ".env"), override=True) + + logger.debug(f"Starting run command with entrypoint: {entrypoint}") + logger.debug(f"Input: {input}") + logger.debug(f"Resume: {resume}") + # Process through middleware chain + logger.debug("Running middleware chain") result = Middlewares.next("run", entrypoint, input, resume) if result.should_continue: + logger.debug("Middleware chain completed, executing Python script") result = python_run_middleware( entrypoint=entrypoint, input=input, resume=resume ) # Handle result from middleware if result.error_message: - click.echo(result.error_message, err=True) + logger.error(result.error_message) if result.should_include_stacktrace: - click.echo(traceback.format_exc(), err=True) + logger.error(traceback.format_exc()) click.get_current_context().exit(1) if result.info_message: - click.echo(result.info_message) + logger.info(result.info_message) # If middleware chain completed but didn't handle the request if result.should_continue: + logger.error("Could not process the request with any available handler") click.echo("Error: Could not process the request with any available handler.") click.get_current_context().exit(1)