Skip to content

Commit 6db317d

Browse files
committed
feat: add entrypoint autodiscover
1 parent 03b641c commit 6db317d

4 files changed

Lines changed: 208 additions & 26 deletions

File tree

packages/uipath/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.10.11"
3+
version = "2.10.12"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath/src/uipath/_cli/cli_run.py

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,38 @@
3434
console = ConsoleLogger()
3535

3636

37+
class _RunDiscoveryError(Exception):
38+
"""Raised when entrypoint auto-discovery fails."""
39+
40+
def __init__(self, entrypoints: list[str]):
41+
self.entrypoints = entrypoints
42+
43+
44+
def _show_run_usage_help(entrypoints: list[str]) -> None:
45+
"""Show available entrypoints with usage examples."""
46+
lines: list[str] = []
47+
48+
if entrypoints:
49+
lines.append("Available entrypoints:")
50+
for name in entrypoints:
51+
lines.append(f" - {name}")
52+
else:
53+
lines.append(
54+
"No entrypoints found. "
55+
"Add a 'functions' or 'agents' section to your config file "
56+
"(e.g. uipath.json, langgraph.json) "
57+
"or MCP slugs to mcp.json."
58+
)
59+
60+
lines.append(
61+
"\nUsage: uipath run <entrypoint> <input_arguments> [-f <input_json_file_path>]"
62+
)
63+
if entrypoints:
64+
lines.append(f"Example: uipath run {entrypoints[0]}")
65+
66+
click.echo("\n".join(lines))
67+
68+
3769
@click.command()
3870
@click.argument("entrypoint", required=False)
3971
@click.argument("input", required=False, default=None)
@@ -125,11 +157,6 @@ def run(
125157
return
126158

127159
if result.should_continue:
128-
if not entrypoint:
129-
console.error("""No entrypoint specified. Please provide the path to the Python function.
130-
Usage: `uipath run <entrypoint> <input_arguments> [-f <input_json_file_path>]`""")
131-
return
132-
133160
try:
134161

135162
async def execute_runtime(
@@ -187,14 +214,23 @@ async def execute() -> None:
187214
factory: UiPathRuntimeFactoryProtocol | None = None
188215
try:
189216
factory = UiPathRuntimeFactoryRegistry.get(context=ctx)
217+
218+
resolved_entrypoint = entrypoint
219+
if not resolved_entrypoint:
220+
available = factory.discover_entrypoints()
221+
if len(available) == 1:
222+
resolved_entrypoint = available[0]
223+
else:
224+
raise _RunDiscoveryError(available)
225+
190226
factory_settings = await factory.get_settings()
191227
trace_settings = (
192228
factory_settings.trace_settings
193229
if factory_settings
194230
else None
195231
)
196232
runtime = await factory.new_runtime(
197-
entrypoint,
233+
resolved_entrypoint,
198234
ctx.conversation_id or ctx.job_id or "default",
199235
)
200236

@@ -230,12 +266,17 @@ async def execute() -> None:
230266

231267
asyncio.run(execute())
232268

269+
except _RunDiscoveryError as e:
270+
_show_run_usage_help(e.entrypoints)
271+
return
233272
except UiPathRuntimeError as e:
234273
console.error(f"{e.error_info.title} - {e.error_info.detail}")
274+
return
235275
except Exception as e:
236276
console.error(
237277
f"Error: Unexpected error occurred - {str(e)}", include_traceback=True
238278
)
279+
return
239280

240281
console.success("Successful execution.")
241282

packages/uipath/tests/cli/test_run.py

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# type: ignore
22
import os
3-
from unittest.mock import patch
3+
from contextlib import asynccontextmanager
4+
from unittest.mock import AsyncMock, Mock, patch
45

56
import pytest
67
from click.testing import CliRunner
@@ -9,6 +10,41 @@
910
from uipath._cli.middlewares import MiddlewareResult
1011

1112

13+
def _middleware_continue():
14+
return MiddlewareResult(
15+
should_continue=True,
16+
error_message=None,
17+
should_include_stacktrace=False,
18+
)
19+
20+
21+
async def _empty_async_gen(*args, **kwargs):
22+
"""An async generator that yields nothing (simulates empty runtime.stream)."""
23+
return
24+
yield # noqa: unreachable - makes this an async generator
25+
26+
27+
def _make_mock_factory(entrypoints: list[str]):
28+
"""Create a mock runtime factory with given entrypoints."""
29+
mock_factory = Mock()
30+
mock_factory.discover_entrypoints.return_value = entrypoints
31+
mock_factory.get_settings = AsyncMock(return_value=None)
32+
mock_factory.dispose = AsyncMock()
33+
34+
mock_runtime = Mock()
35+
mock_runtime.execute = AsyncMock(return_value=Mock(status="SUCCESSFUL"))
36+
mock_runtime.stream = Mock(side_effect=_empty_async_gen)
37+
mock_runtime.dispose = AsyncMock()
38+
mock_factory.new_runtime = AsyncMock(return_value=mock_runtime)
39+
40+
return mock_factory
41+
42+
43+
@asynccontextmanager
44+
async def _mock_resource_overwrites_context(*args, **kwargs):
45+
yield
46+
47+
1248
@pytest.fixture
1349
def entrypoint():
1450
return "main"
@@ -142,14 +178,81 @@ def test_run_input_file_success(
142178
assert "Successful execution." in result.output
143179

144180
class TestMiddleware:
145-
def test_no_entrypoint(self, runner: CliRunner, temp_dir: str):
181+
def test_autodiscover_entrypoint(self, runner: CliRunner, temp_dir: str):
182+
"""When exactly one entrypoint exists, it is auto-resolved."""
146183
with runner.isolated_filesystem(temp_dir=temp_dir):
147-
result = runner.invoke(cli, ["run"])
148-
assert result.exit_code == 1
149-
assert (
150-
"No entrypoint specified" in result.output
151-
or "Missing argument" in result.output
184+
mock_factory = _make_mock_factory(["my_agent"])
185+
186+
with (
187+
patch(
188+
"uipath._cli.cli_run.Middlewares.next",
189+
return_value=_middleware_continue(),
190+
),
191+
patch(
192+
"uipath._cli.cli_run.UiPathRuntimeFactoryRegistry.get",
193+
return_value=mock_factory,
194+
),
195+
patch(
196+
"uipath._cli.cli_run.ResourceOverwritesContext",
197+
side_effect=_mock_resource_overwrites_context,
198+
),
199+
):
200+
result = runner.invoke(cli, ["run"])
201+
202+
assert result.exit_code == 0, (
203+
f"output: {result.output!r}, exception: {result.exception}"
152204
)
205+
assert "Successful execution." in result.output
206+
mock_factory.new_runtime.assert_awaited_once()
207+
assert mock_factory.new_runtime.call_args[0][0] == "my_agent"
208+
209+
def test_no_entrypoint_multiple_available(
210+
self, runner: CliRunner, temp_dir: str
211+
):
212+
"""When multiple entrypoints exist and none specified, show usage help."""
213+
with runner.isolated_filesystem(temp_dir=temp_dir):
214+
mock_factory = _make_mock_factory(["agent_a", "agent_b"])
215+
216+
with (
217+
patch(
218+
"uipath._cli.cli_run.Middlewares.next",
219+
return_value=_middleware_continue(),
220+
),
221+
patch(
222+
"uipath._cli.cli_run.UiPathRuntimeFactoryRegistry.get",
223+
return_value=mock_factory,
224+
),
225+
):
226+
result = runner.invoke(cli, ["run"])
227+
228+
assert result.exit_code == 0
229+
assert "Available entrypoints:" in result.output
230+
assert "agent_a" in result.output
231+
assert "agent_b" in result.output
232+
assert "Usage: uipath run" in result.output
233+
mock_factory.new_runtime.assert_not_awaited()
234+
235+
def test_no_entrypoint_none_available(self, runner: CliRunner, temp_dir: str):
236+
"""When no entrypoints exist and none specified, show usage help."""
237+
with runner.isolated_filesystem(temp_dir=temp_dir):
238+
mock_factory = _make_mock_factory([])
239+
240+
with (
241+
patch(
242+
"uipath._cli.cli_run.Middlewares.next",
243+
return_value=_middleware_continue(),
244+
),
245+
patch(
246+
"uipath._cli.cli_run.UiPathRuntimeFactoryRegistry.get",
247+
return_value=mock_factory,
248+
),
249+
):
250+
result = runner.invoke(cli, ["run"])
251+
252+
assert result.exit_code == 0
253+
assert "No entrypoints found" in result.output
254+
assert "Usage: uipath run" in result.output
255+
mock_factory.new_runtime.assert_not_awaited()
153256

154257
def test_script_not_found(
155258
self, runner: CliRunner, temp_dir: str, entrypoint: str

packages/uipath/uv.lock

Lines changed: 50 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)