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
2 changes: 1 addition & 1 deletion packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath"
version = "2.10.11"
version = "2.10.12"
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
49 changes: 43 additions & 6 deletions packages/uipath/src/uipath/_cli/cli_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,37 @@
console = ConsoleLogger()


class _RunDiscoveryError(Exception):
"""Raised when entrypoint auto-discovery fails."""

def __init__(self, entrypoints: list[str]):
self.entrypoints = entrypoints


def _show_run_usage_help(entrypoints: list[str]) -> None:
"""Show available entrypoints with usage examples."""
lines: list[str] = []

if entrypoints:
lines.append("Available entrypoints:")
for name in entrypoints:
lines.append(f" - {name}")
else:
lines.append(
"No entrypoints found. "
"Add a 'functions' or 'agents' section to your config file "
"(e.g. uipath.json, langgraph.json)."
)

lines.append(
"\nUsage: uipath run <entrypoint> <input_arguments> [-f <input_json_file_path>]"
)
if entrypoints:
lines.append(f"Example: uipath run {entrypoints[0]}")

click.echo("\n".join(lines))


@click.command()
@click.argument("entrypoint", required=False)
@click.argument("input", required=False, default=None)
Expand Down Expand Up @@ -125,11 +156,6 @@ def run(
return

if result.should_continue:
if not entrypoint:
console.error("""No entrypoint specified. Please provide the path to the Python function.
Usage: `uipath run <entrypoint> <input_arguments> [-f <input_json_file_path>]`""")
return

try:

async def execute_runtime(
Expand Down Expand Up @@ -187,14 +213,23 @@ async def execute() -> None:
factory: UiPathRuntimeFactoryProtocol | None = None
try:
factory = UiPathRuntimeFactoryRegistry.get(context=ctx)

resolved_entrypoint = entrypoint
if not resolved_entrypoint:
available = factory.discover_entrypoints()
if len(available) == 1:
resolved_entrypoint = available[0]
else:
raise _RunDiscoveryError(available)

factory_settings = await factory.get_settings()
trace_settings = (
factory_settings.trace_settings
if factory_settings
else None
)
runtime = await factory.new_runtime(
entrypoint,
resolved_entrypoint,
ctx.conversation_id or ctx.job_id or "default",
)

Expand Down Expand Up @@ -230,6 +265,8 @@ async def execute() -> None:

asyncio.run(execute())

except _RunDiscoveryError as e:
_show_run_usage_help(e.entrypoints)
except UiPathRuntimeError as e:
console.error(f"{e.error_info.title} - {e.error_info.detail}")
except Exception as e:
Expand Down
117 changes: 110 additions & 7 deletions packages/uipath/tests/cli/test_run.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# type: ignore
import os
from unittest.mock import patch
from contextlib import asynccontextmanager
from unittest.mock import AsyncMock, Mock, patch

import pytest
from click.testing import CliRunner
Expand All @@ -9,6 +10,41 @@
from uipath._cli.middlewares import MiddlewareResult


def _middleware_continue():
return MiddlewareResult(
should_continue=True,
error_message=None,
should_include_stacktrace=False,
)


async def _empty_async_gen(*args, **kwargs):
"""An async generator that yields nothing (simulates empty runtime.stream)."""
return
yield # noqa: unreachable - makes this an async generator


def _make_mock_factory(entrypoints: list[str]):
"""Create a mock runtime factory with given entrypoints."""
mock_factory = Mock()
mock_factory.discover_entrypoints.return_value = entrypoints
mock_factory.get_settings = AsyncMock(return_value=None)
mock_factory.dispose = AsyncMock()

mock_runtime = Mock()
mock_runtime.execute = AsyncMock(return_value=Mock(status="SUCCESSFUL"))
mock_runtime.stream = Mock(side_effect=_empty_async_gen)
mock_runtime.dispose = AsyncMock()
mock_factory.new_runtime = AsyncMock(return_value=mock_runtime)

return mock_factory


@asynccontextmanager
async def _mock_resource_overwrites_context(*args, **kwargs):
yield


@pytest.fixture
def entrypoint():
return "main"
Expand Down Expand Up @@ -142,14 +178,81 @@ def test_run_input_file_success(
assert "Successful execution." in result.output

class TestMiddleware:
def test_no_entrypoint(self, runner: CliRunner, temp_dir: str):
def test_autodiscover_entrypoint(self, runner: CliRunner, temp_dir: str):
"""When exactly one entrypoint exists, it is auto-resolved."""
with runner.isolated_filesystem(temp_dir=temp_dir):
result = runner.invoke(cli, ["run"])
assert result.exit_code == 1
assert (
"No entrypoint specified" in result.output
or "Missing argument" in result.output
mock_factory = _make_mock_factory(["my_agent"])

with (
patch(
"uipath._cli.cli_run.Middlewares.next",
return_value=_middleware_continue(),
),
patch(
"uipath._cli.cli_run.UiPathRuntimeFactoryRegistry.get",
return_value=mock_factory,
),
patch(
"uipath._cli.cli_run.ResourceOverwritesContext",
side_effect=_mock_resource_overwrites_context,
),
):
result = runner.invoke(cli, ["run"])

assert result.exit_code == 0, (
f"output: {result.output!r}, exception: {result.exception}"
)
assert "Successful execution." in result.output
mock_factory.new_runtime.assert_awaited_once()
assert mock_factory.new_runtime.call_args[0][0] == "my_agent"

def test_no_entrypoint_multiple_available(
self, runner: CliRunner, temp_dir: str
):
"""When multiple entrypoints exist and none specified, show usage help."""
with runner.isolated_filesystem(temp_dir=temp_dir):
mock_factory = _make_mock_factory(["agent_a", "agent_b"])

with (
patch(
"uipath._cli.cli_run.Middlewares.next",
return_value=_middleware_continue(),
),
patch(
"uipath._cli.cli_run.UiPathRuntimeFactoryRegistry.get",
return_value=mock_factory,
),
):
result = runner.invoke(cli, ["run"])

assert result.exit_code == 0
assert "Available entrypoints:" in result.output
assert "agent_a" in result.output
assert "agent_b" in result.output
assert "Usage: uipath run" in result.output
mock_factory.new_runtime.assert_not_awaited()

def test_no_entrypoint_none_available(self, runner: CliRunner, temp_dir: str):
"""When no entrypoints exist and none specified, show usage help."""
with runner.isolated_filesystem(temp_dir=temp_dir):
mock_factory = _make_mock_factory([])

with (
patch(
"uipath._cli.cli_run.Middlewares.next",
return_value=_middleware_continue(),
),
patch(
"uipath._cli.cli_run.UiPathRuntimeFactoryRegistry.get",
return_value=mock_factory,
),
):
result = runner.invoke(cli, ["run"])

assert result.exit_code == 0
assert "No entrypoints found" in result.output
assert "Usage: uipath run" in result.output
mock_factory.new_runtime.assert_not_awaited()

def test_script_not_found(
self, runner: CliRunner, temp_dir: str, entrypoint: str
Expand Down
62 changes: 50 additions & 12 deletions packages/uipath/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading