Skip to content
Merged
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
5 changes: 1 addition & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath"
version = "2.0.83"
version = "2.1.0"
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.10"
Expand Down Expand Up @@ -67,9 +67,6 @@ dev = [
"types-toml>=0.10.8",
]

[project.optional-dependencies]
langchain = ["uipath-langchain>=0.0.88,<0.1.0"]

[tool.hatch.build.targets.wheel]
packages = ["src/uipath"]

Expand Down
20 changes: 20 additions & 0 deletions src/uipath/_cli/_runtime/_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,16 @@ class UiPathRuntimeContext(BaseModel):
trace_context: Optional[UiPathTraceContext] = None
tracing_enabled: Union[bool, str] = False
resume: bool = False
debug: bool = False
config_path: str = "uipath.json"
runtime_dir: Optional[str] = "__uipath"
logs_file: Optional[str] = "execution.log"
logs_min_level: Optional[str] = "INFO"
output_file: str = "output.json"
state_file: str = "state.db"
result: Optional[UiPathRuntimeResult] = None
execution_output_file: Optional[str] = None
input_file: Optional[str] = None

model_config = {"arbitrary_types_allowed": True}

Expand Down Expand Up @@ -295,6 +298,18 @@ async def __aenter__(self):
Returns:
The runtime instance
"""
# Read the input from file if provided
if self.context.input_file:
_, file_extension = os.path.splitext(self.context.input_file)
if file_extension != ".json":
raise UiPathRuntimeError(
code="INVALID_INPUT_FILE_EXTENSION",
title="Invalid Input File Extension",
detail="The provided input file must be in JSON format.",
)
with open(self.context.input_file) as f:
self.context.input = f.read()

# Intercept all stdout/stderr/logs and write them to a file (runtime), stdout (debug)
self.logs_interceptor = LogsInterceptor(
min_level=self.context.logs_min_level,
Expand Down Expand Up @@ -370,6 +385,11 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
with open(self.output_file_path, "w") as f:
json.dump(content, f, indent=2, default=str)

# Write the execution output to file if requested
if self.context.execution_output_file:
with open(self.context.execution_output_file, "w") as f:
json.dump(execution_result.output or {}, f, indent=2, default=str)

# Don't suppress exceptions
return False

Expand Down
4 changes: 4 additions & 0 deletions src/uipath/_cli/_utils/_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ def setup_debugging(debug: bool, debug_port: int = 5678) -> bool:
console.info(f"🐛 Debug server started on port {debug_port}")
console.info("📌 Waiting for debugger to attach...")
console.info(" - VS Code: Run -> Start Debugging -> Python: Remote Attach")
console.link(
" CLI Documentation reference: ",
"https://uipath.github.io/uipath-python/cli/#run",
)

debugpy.wait_for_client()
console.success("Debugger attached successfully!")
Expand Down
38 changes: 30 additions & 8 deletions src/uipath/_cli/cli_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def python_run_middleware(
entrypoint: Optional[str],
input: Optional[str],
resume: bool,
**kwargs,
) -> MiddlewareResult:
"""Middleware to handle Python script execution.

Expand Down Expand Up @@ -70,6 +71,8 @@ async def execute():
context.resume = resume
context.job_id = env.get("UIPATH_JOB_KEY")
context.trace_id = env.get("UIPATH_TRACE_ID")
context.input_file = kwargs.get("input_file", None)
context.execution_output_file = kwargs.get("execution_output_file", None)
context.tracing_enabled = env.get("UIPATH_TRACING_ENABLED", True)
context.trace_context = UiPathTraceContext(
trace_id=env.get("UIPATH_TRACE_ID"),
Expand Down Expand Up @@ -118,6 +121,18 @@ async def execute():
type=click.Path(exists=True),
help="File path for the .json input",
)
@click.option(
"--input-file",
required=False,
type=click.Path(exists=True),
help="Alias for '-f/--file' arguments",
)
@click.option(
"--output-file",
required=False,
type=click.Path(exists=False),
help="File path where the output will be written",
)
@click.option(
"--debug",
is_flag=True,
Expand All @@ -135,29 +150,36 @@ def run(
input: Optional[str],
resume: bool,
file: Optional[str],
input_file: Optional[str],
output_file: Optional[str],
debug: bool,
debug_port: int,
) -> None:
"""Execute the project."""
if file:
_, file_extension = os.path.splitext(file)
if file_extension != ".json":
console.error("Input file extension must be '.json'.")
with open(file) as f:
input = f.read()
input_file = file or input_file
# Setup debugging if requested

if not setup_debugging(debug, debug_port):
console.error(f"Failed to start debug server on port {debug_port}")

# Process through middleware chain
result = Middlewares.next("run", entrypoint, input, resume)
result = Middlewares.next(
"run",
entrypoint,
input,
resume,
debug=debug,
debug_port=debug_port,
input_file=input_file,
execution_output_file=output_file,
)

if result.should_continue:
result = python_run_middleware(
entrypoint=entrypoint,
input=input,
resume=resume,
input_file=input_file,
execution_output_file=output_file,
)

# Handle result from middleware
Expand Down
34 changes: 30 additions & 4 deletions tests/cli/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ def test_run_input_file_not_found(
entrypoint: str,
):
with runner.isolated_filesystem(temp_dir=temp_dir):
file_path = os.path.join(temp_dir, entrypoint)
with open(file_path, "w") as f:
f.write("script content")
result = runner.invoke(run, [entrypoint, "--file", "not-here.json"])
assert result.exit_code != 0
assert "Error: Invalid value for '-f' / '--file'" in result.output
Expand All @@ -65,12 +68,15 @@ def test_run_invalid_input_file(
):
file_name = "not-json.txt"
with runner.isolated_filesystem(temp_dir=temp_dir):
script_file_path = os.path.join(temp_dir, entrypoint)
with open(script_file_path, "w") as f:
f.write("script content")
file_path = os.path.join(temp_dir, file_name)
with open(file_path, "w") as f:
f.write("file content")
result = runner.invoke(run, [entrypoint, "--file", file_path])
result = runner.invoke(run, [script_file_path, "--file", file_path])
assert result.exit_code == 1
assert "Input file extension must be '.json'." in result.output
assert "Invalid Input File Extension" in result.output

def test_run_input_file_success(
self,
Expand Down Expand Up @@ -100,7 +106,14 @@ def test_run_input_file_success(
assert "Successful execution." in result.output
assert mock_middleware.call_count == 1
assert mock_middleware.call_args == mock.call(
"run", entrypoint, json_content, False
"run",
entrypoint,
"{}",
False,
debug=False,
debug_port=5678,
input_file=file_path,
execution_output_file=None,
)

class TestMiddleware:
Expand Down Expand Up @@ -134,6 +147,7 @@ def test_successful_execution(
simple_script: str,
):
input_file_name = "input.json"
output_file_name = "output.json"
input_json_content = """
{
"message": "Hello world",
Expand All @@ -142,6 +156,7 @@ def test_successful_execution(
with runner.isolated_filesystem(temp_dir=temp_dir):
# create input file
input_file_path = os.path.join(temp_dir, input_file_name)
output_file_path = os.path.join(temp_dir, output_file_name)
with open(input_file_path, "w") as f:
f.write(input_json_content)
# Create test script
Expand All @@ -152,11 +167,22 @@ def test_successful_execution(
with open("uipath.json", "w") as f:
f.write(uipath_json.to_json())
result = runner.invoke(
run, [script_file_path, "--file", input_file_path]
run,
[
script_file_path,
"--input-file",
input_file_path,
"--output-file",
output_file_path,
],
)
assert result.exit_code == 0
assert "Successful execution." in result.output
assert result.output.count("Hello world") == 2
assert os.path.exists(output_file_path)
with open(output_file_path, "r") as f:
output = f.read()
assert output.count("Hello world") == 2

def test_no_main_function_found(
self,
Expand Down
Loading