Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions packages/uipath/src/uipath/_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,78 @@ def get_command(self, ctx, cmd_name):
return _load_command(cmd_name)
return None

def invoke(self, ctx):
try:
result = super().invoke(ctx)

# After successful command execution, emit collected output
cli_ctx = ctx.obj
if isinstance(cli_ctx, CliContext) and cli_ctx.output_format not in (
"table",
"csv",
):
from uipath._cli._utils._console import CliLogger

logger = CliLogger()
logger.emit()

return result

except (SystemExit, click.exceptions.Exit):
raise

except click.ClickException as e:
cli_ctx = getattr(ctx, "obj", None)
if isinstance(cli_ctx, CliContext) and cli_ctx.output_format not in (
"table",
"csv",
):
import json as json_mod

from uipath._cli._utils._console import CliLogger

logger = CliLogger()
error_output = {"status": "error", "error": e.format_message()}
if logger._messages:
error_output["messages"] = list(logger._messages)

click.echo(
json_mod.dumps(error_output, indent=2, default=str), err=True
)
ctx.exit(1)
else:
raise

except Exception as e:
cli_ctx = getattr(ctx, "obj", None)
if isinstance(cli_ctx, CliContext) and cli_ctx.output_format not in (
"table",
"csv",
):
import json as json_mod

from uipath._cli._utils._console import CLIError, CliLogger

logger = CliLogger()

if isinstance(e, CLIError):
messages = e.messages
error_msg = e.message
else:
messages = list(logger._messages)
error_msg = str(e)

error_output = {"status": "error", "error": error_msg}
if messages:
error_output["messages"] = messages

click.echo(
json_mod.dumps(error_output, indent=2, default=str), err=True
)
ctx.exit(1)
else:
raise

def format_help(self, ctx, formatter):
format_value = _get_format_from_argv()

Expand Down Expand Up @@ -193,6 +265,12 @@ def cli(

setup_logging(should_debug=debug)

if format != "table":
from uipath._cli._utils._console import CliLogger

logger = CliLogger()
logger.output_mode = format # "json" or "csv"

if lv:
try:
version = importlib.metadata.version("uipath-langchain")
Expand Down
4 changes: 2 additions & 2 deletions packages/uipath/src/uipath/_cli/_auth/_auth_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from uipath._cli._auth._oidc_utils import OidcUtils
from uipath._cli._auth._url_utils import extract_org_tenant, resolve_domain
from uipath._cli._auth._utils import get_parsed_token_data
from uipath._cli._utils._console import ConsoleLogger
from uipath._cli._utils._console import CliLogger
from uipath._utils._auth import update_env_file
from uipath.platform.common import ExternalApplicationService, TokenData

Expand All @@ -26,7 +26,7 @@ def __init__(
scope: str | None = None,
):
self._force = force
self._console = ConsoleLogger()
self._console = CliLogger()
self._client_id = client_id
self._client_secret = client_secret
self._base_url = base_url
Expand Down
4 changes: 2 additions & 2 deletions packages/uipath/src/uipath/_cli/_auth/_auth_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
)

from ..._utils._auth import update_env_file
from .._utils._console import ConsoleLogger
from .._utils._console import CliLogger
from ._oidc_utils import OidcUtils
from ._utils import get_auth_data, get_parsed_token_data, update_auth_file

Expand All @@ -41,7 +41,7 @@ def __init__(
self.domain = domain
self.access_token = access_token
self.prt_id = prt_id
self._console = ConsoleLogger()
self._console = CliLogger()
self._identity_service = IdentityService(domain)
self._portal_service = PlatformPortalService(domain)
self._tenants_and_organizations = None
Expand Down
4 changes: 2 additions & 2 deletions packages/uipath/src/uipath/_cli/_auth/_oidc_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from uipath.platform.orchestrator import get_server_info_async

from .._utils._console import ConsoleLogger
from .._utils._console import CliLogger
from ._models import AuthConfig
from ._url_utils import build_service_url

Expand Down Expand Up @@ -88,7 +88,7 @@ async def _select_config_file(domain: str) -> str:


class OidcUtils:
_console = ConsoleLogger()
_console = CliLogger()

@classmethod
def _find_free_port(cls, candidates: list[int]):
Expand Down
4 changes: 2 additions & 2 deletions packages/uipath/src/uipath/_cli/_auth/_url_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from typing import Tuple
from urllib.parse import urlparse

from .._utils._console import ConsoleLogger
from .._utils._console import CliLogger

console = ConsoleLogger()
console = CliLogger()


def resolve_domain(base_url: str | None, environment: str | None) -> str:
Expand Down
4 changes: 2 additions & 2 deletions packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from pydantic.alias_generators import to_camel
from rich.console import Console

from uipath._cli._utils._console import ConsoleLogger
from uipath._cli._utils._console import CliLogger
from uipath._utils import Endpoint, RequestSpec
from uipath._utils.constants import (
ENV_EVAL_BACKEND_URL,
Expand Down Expand Up @@ -92,7 +92,7 @@ class StudioWebProgressReporter:

def __init__(self):
logging.getLogger("uipath._cli.middlewares").setLevel(logging.CRITICAL)
console_logger = ConsoleLogger.get_instance()
console_logger = CliLogger.get_instance()

# Use UIPATH_EVAL_BACKEND_URL for eval-specific routing if set
eval_backend_url = os.getenv(ENV_EVAL_BACKEND_URL)
Expand Down
4 changes: 2 additions & 2 deletions packages/uipath/src/uipath/_cli/_push/sw_file_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from ...platform.errors import EnrichedException
from .._utils._common import get_claim_from_token
from .._utils._console import ConsoleLogger
from .._utils._console import CliLogger
from .._utils._constants import (
AGENT_INITIAL_CODE_VERSION,
SCHEMA_VERSION,
Expand Down Expand Up @@ -66,7 +66,7 @@ def __init__(
"""
self.directory = directory
self.include_uv_lock = include_uv_lock
self.console = ConsoleLogger()
self.console = CliLogger()
self._studio_client = studio_client or StudioClient(project_id)
self._project_structure: ProjectStructure | None = None

Expand Down
8 changes: 4 additions & 4 deletions packages/uipath/src/uipath/_cli/_utils/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from ..._utils.constants import ENV_UIPATH_ACCESS_TOKEN
from ..models.runtime_schema import EntryPoint
from ..spinner import Spinner
from ._console import ConsoleLogger
from ._console import CliLogger
from ._studio_project import (
NonCodedAgentProjectException,
StudioClient,
Expand Down Expand Up @@ -126,7 +126,7 @@ def determine_project_type(entrypoints: list[EntryPoint]) -> str:
chosen_type = entrypoints[0].type

if len(unique_types) > 1:
console = ConsoleLogger()
console = CliLogger()
types_str = ", ".join(sorted(unique_types))
console.warning(
f"Mixed entrypoint types detected: [{types_str}]. "
Expand All @@ -141,7 +141,7 @@ async def ensure_coded_agent_project(studio_client: StudioClient):
try:
await studio_client.ensure_coded_agent_project_async()
except NonCodedAgentProjectException:
console = ConsoleLogger()
console = CliLogger()
console.error(
"The targeted Studio Web project is not of type coded agent. Please check the UIPATH_PROJECT_ID environment variable."
)
Expand Down Expand Up @@ -182,7 +182,7 @@ async def may_override_files(
except (ValueError, TypeError):
formatted_date = remote_metadata.last_push_date

console = ConsoleLogger()
console = CliLogger()
console.warning("Your local version is behind the remote version.")
console.info(f" Remote version: {remote_metadata.code_version}")
console.info(f" Local version: {local_version_display}")
Expand Down
81 changes: 72 additions & 9 deletions packages/uipath/src/uipath/_cli/_utils/_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,37 @@ class LogLevel(Enum):
MAGIC = "✨"


class ConsoleLogger:
class CLIError(Exception):
"""Raised by CliLogger.error() in non-console output modes.

Caught by LazyGroup.invoke() to emit structured error output.
"""

def __init__(self, message: str, messages: list[dict[str, str]] | None = None):
self.message = message
self.messages = messages or []
super().__init__(message)


class CliLogger:
"""A singleton wrapper class for terminal output with emoji support and spinners."""

# Class variable to hold the singleton instance
_instance: ConsoleLogger | None = None
_instance: CliLogger | None = None

def __new__(cls: Type[ConsoleLogger]) -> ConsoleLogger:
"""Ensure only one instance of ConsoleLogger is created.
def __new__(cls: Type[CliLogger]) -> CliLogger:
"""Ensure only one instance of CliLogger is created.

Returns:
The singleton instance of ConsoleLogger
The singleton instance of CliLogger
"""
if cls._instance is None:
cls._instance = super(ConsoleLogger, cls).__new__(cls)
cls._instance = super(CliLogger, cls).__new__(cls)
cls._instance._initialized = False
return cls._instance

def __init__(self):
"""Initialize the ConsoleLogger (only once)."""
"""Initialize the CliLogger (only once)."""
# Only initialize once
if not getattr(self, "_initialized", False):
self._console = Console()
Expand All @@ -59,6 +71,21 @@ def __init__(self):
self._progress: Progress | None = None
self._progress_tasks: dict[str, TaskID] = {}
self._initialized = True
self._output_mode: str = "console"
self._messages: list[dict[str, str]] = []
self._result: Any = None

@property
def output_mode(self) -> str:
"""Get the current output mode."""
return self._output_mode

@output_mode.setter
def output_mode(self, value: str) -> None:
"""Set the output mode and reset collected state."""
self._output_mode = value
self._messages = []
self._result = None

def _stop_spinner_if_active(self) -> None:
"""Internal method to stop the spinner if it's active."""
Expand All @@ -83,6 +110,11 @@ def log(
level: The log level (determines the emoji)
fg: Optional foreground color for the message
"""
if self._output_mode != "console":
level_name = level.name.lower()
self._messages.append({"level": level_name, "message": message})
return

# Stop any active spinner before logging
self._stop_spinner_if_active()

Expand Down Expand Up @@ -119,6 +151,9 @@ def error(self, message: str, include_traceback: bool = False) -> NoReturn:
)
message = f"{message}\n{tb}"

if self._output_mode != "console":
raise CLIError(message, messages=list(self._messages))

self.log(message, LogLevel.ERROR, "red")
click.get_current_context().exit(1)

Expand Down Expand Up @@ -216,6 +251,10 @@ def spinner(self, message: str = "") -> Iterator[None]:
Yields:
None
"""
if self._output_mode != "console":
yield
return

try:
# Stop any existing spinner before starting a new one
self._stop_spinner_if_active()
Expand Down Expand Up @@ -280,9 +319,30 @@ def evaluation_progress(
finally:
self._stop_progress_if_active()

def set_result(self, data: Any) -> None:
"""Store structured result data for JSON output."""
self._result = data

def emit(self) -> None:
"""Emit collected output. Only does something in non-console modes."""
if self._output_mode == "console":
return

import json

output: dict[str, Any] = {"status": "success"}
if self._messages:
output["messages"] = self._messages
if self._result is not None:
output["data"] = self._result

click.echo(json.dumps(output, indent=2, default=str))
self._messages = []
self._result = None

@classmethod
def get_instance(cls) -> "ConsoleLogger":
"""Get the singleton instance of ConsoleLogger.
def get_instance(cls) -> "CliLogger":
"""Get the singleton instance of CliLogger.

Returns:
The singleton instance
Expand All @@ -292,6 +352,9 @@ def get_instance(cls) -> "ConsoleLogger":
return cls._instance


ConsoleLogger = CliLogger


class EvaluationProgressManager:
"""Manager for evaluation progress updates."""

Expand Down
4 changes: 2 additions & 2 deletions packages/uipath/src/uipath/_cli/_utils/_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import os

from ._console import ConsoleLogger
from ._console import CliLogger

console = ConsoleLogger()
console = CliLogger()


def setup_debugging(debug: bool, debug_port: int = 5678) -> bool:
Expand Down
4 changes: 2 additions & 2 deletions packages/uipath/src/uipath/_cli/_utils/_folders.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import Any, Optional, Tuple

from ._console import ConsoleLogger
from ._console import CliLogger

console = ConsoleLogger()
console = CliLogger()


async def get_personal_workspace_info_async() -> Tuple[Optional[str], Optional[str]]:
Expand Down
Loading
Loading