Skip to content

Commit 82f3b85

Browse files
committed
feat: debug and output file support
1 parent 714a000 commit 82f3b85

7 files changed

Lines changed: 106 additions & 1315 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.0.82"
3+
version = "2.1.0"
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.10"
@@ -67,9 +67,6 @@ dev = [
6767
"types-toml>=0.10.8",
6868
]
6969

70-
[project.optional-dependencies]
71-
langchain = ["uipath-langchain>=0.0.88,<0.1.0"]
72-
7370
[tool.hatch.build.targets.wheel]
7471
packages = ["src/uipath"]
7572

src/uipath/_cli/_runtime/_contracts.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,16 @@ class UiPathRuntimeContext(BaseModel):
148148
trace_context: Optional[UiPathTraceContext] = None
149149
tracing_enabled: Union[bool, str] = False
150150
resume: bool = False
151+
debug: bool = False
151152
config_path: str = "uipath.json"
152153
runtime_dir: Optional[str] = "__uipath"
153154
logs_file: Optional[str] = "execution.log"
154155
logs_min_level: Optional[str] = "INFO"
155156
output_file: str = "output.json"
156157
state_file: str = "state.db"
157158
result: Optional[UiPathRuntimeResult] = None
159+
execution_output_file: Optional[str] = None
160+
input_file: Optional[str] = None
158161

159162
model_config = {"arbitrary_types_allowed": True}
160163

@@ -295,6 +298,18 @@ async def __aenter__(self):
295298
Returns:
296299
The runtime instance
297300
"""
301+
# Read the input from file if provided
302+
if self.context.input_file:
303+
_, file_extension = os.path.splitext(self.context.input_file)
304+
if file_extension != ".json":
305+
raise UiPathRuntimeError(
306+
code="INVALID_INPUT_FILE_EXTENSION",
307+
title="Invalid Input File Extension",
308+
detail="The provided input file must be in JSON format.",
309+
)
310+
with open(self.context.input_file) as f:
311+
self.context.input = f.read()
312+
298313
# Intercept all stdout/stderr/logs and write them to a file (runtime), stdout (debug)
299314
self.logs_interceptor = LogsInterceptor(
300315
min_level=self.context.logs_min_level,
@@ -370,6 +385,11 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
370385
with open(self.output_file_path, "w") as f:
371386
json.dump(content, f, indent=2, default=str)
372387

388+
# Write the execution output to file if requested
389+
if self.context.execution_output_file:
390+
with open(self.context.execution_output_file, "w") as f:
391+
json.dump(execution_result.output or {}, f, indent=2, default=str)
392+
373393
# Don't suppress exceptions
374394
return False
375395

src/uipath/_cli/_utils/_debug.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ def setup_debugging(debug: bool, debug_port: int = 5678) -> bool:
4242
console.info(f"🐛 Debug server started on port {debug_port}")
4343
console.info("📌 Waiting for debugger to attach...")
4444
console.info(" - VS Code: Run -> Start Debugging -> Python: Remote Attach")
45+
console.link(
46+
" CLI Documentation reference: ",
47+
"https://uipath.github.io/uipath-python/cli/#run",
48+
)
4549

4650
debugpy.wait_for_client()
4751
console.success("Debugger attached successfully!")

src/uipath/_cli/cli_run.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
UiPathTraceContext,
2222
)
2323
from ._runtime._runtime import UiPathRuntime
24+
from ._utils._common import serialize_object
2425
from ._utils._console import ConsoleLogger
2526
from .middlewares import MiddlewareResult, Middlewares
2627

@@ -32,6 +33,7 @@ def python_run_middleware(
3233
entrypoint: Optional[str],
3334
input: Optional[str],
3435
resume: bool,
36+
**kwargs,
3537
) -> MiddlewareResult:
3638
"""Middleware to handle Python script execution.
3739
@@ -70,6 +72,8 @@ async def execute():
7072
context.resume = resume
7173
context.job_id = env.get("UIPATH_JOB_KEY")
7274
context.trace_id = env.get("UIPATH_TRACE_ID")
75+
context.input_file = kwargs.get("input_file", None)
76+
context.execution_output_file = kwargs.get("execution_output_file", None)
7377
context.tracing_enabled = env.get("UIPATH_TRACING_ENABLED", True)
7478
context.trace_context = UiPathTraceContext(
7579
trace_id=env.get("UIPATH_TRACE_ID"),
@@ -85,12 +89,14 @@ async def execute():
8589
)
8690
context.logs_min_level = env.get("LOG_LEVEL", "INFO")
8791
async with UiPathRuntime.from_context(context) as runtime:
88-
await runtime.execute()
92+
return await runtime.execute()
8993

90-
asyncio.run(execute())
94+
result = asyncio.run(execute())
9195

9296
# Return success
93-
return MiddlewareResult(should_continue=False)
97+
return MiddlewareResult(
98+
should_continue=False, output=serialize_object(result.output)
99+
)
94100

95101
except UiPathRuntimeError as e:
96102
return MiddlewareResult(
@@ -118,6 +124,18 @@ async def execute():
118124
type=click.Path(exists=True),
119125
help="File path for the .json input",
120126
)
127+
@click.option(
128+
"--input-file",
129+
required=False,
130+
type=click.Path(exists=True),
131+
help="Alias for '-f/--file' arguments",
132+
)
133+
@click.option(
134+
"--output-file",
135+
required=False,
136+
type=click.Path(exists=False),
137+
help="File path where the output will be written",
138+
)
121139
@click.option(
122140
"--debug",
123141
is_flag=True,
@@ -135,29 +153,36 @@ def run(
135153
input: Optional[str],
136154
resume: bool,
137155
file: Optional[str],
156+
input_file: Optional[str],
157+
output_file: Optional[str],
138158
debug: bool,
139159
debug_port: int,
140160
) -> None:
141161
"""Execute the project."""
142-
if file:
143-
_, file_extension = os.path.splitext(file)
144-
if file_extension != ".json":
145-
console.error("Input file extension must be '.json'.")
146-
with open(file) as f:
147-
input = f.read()
162+
input_file = file or input_file
148163
# Setup debugging if requested
149-
150164
if not setup_debugging(debug, debug_port):
151165
console.error(f"Failed to start debug server on port {debug_port}")
152166

153167
# Process through middleware chain
154-
result = Middlewares.next("run", entrypoint, input, resume)
168+
result = Middlewares.next(
169+
"run",
170+
entrypoint,
171+
input,
172+
resume,
173+
debug=debug,
174+
debug_port=debug_port,
175+
input_file=input_file,
176+
execution_output_file=output_file,
177+
)
155178

156179
if result.should_continue:
157180
result = python_run_middleware(
158181
entrypoint=entrypoint,
159182
input=input,
160183
resume=resume,
184+
input_file=input_file,
185+
execution_output_file=output_file,
161186
)
162187

163188
# Handle result from middleware

src/uipath/_cli/middlewares.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class MiddlewareResult:
1616
info_message: Optional[str] = None
1717
error_message: Optional[str] = None
1818
should_include_stacktrace: bool = False
19+
output: Optional[str] = None
1920

2021

2122
MiddlewareFunc = Callable[..., MiddlewareResult]

tests/cli/test_run.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ def test_run_input_file_not_found(
5353
entrypoint: str,
5454
):
5555
with runner.isolated_filesystem(temp_dir=temp_dir):
56+
file_path = os.path.join(temp_dir, entrypoint)
57+
with open(file_path, "w") as f:
58+
f.write("script content")
5659
result = runner.invoke(run, [entrypoint, "--file", "not-here.json"])
5760
assert result.exit_code != 0
5861
assert "Error: Invalid value for '-f' / '--file'" in result.output
@@ -65,12 +68,15 @@ def test_run_invalid_input_file(
6568
):
6669
file_name = "not-json.txt"
6770
with runner.isolated_filesystem(temp_dir=temp_dir):
71+
script_file_path = os.path.join(temp_dir, entrypoint)
72+
with open(script_file_path, "w") as f:
73+
f.write("script content")
6874
file_path = os.path.join(temp_dir, file_name)
6975
with open(file_path, "w") as f:
7076
f.write("file content")
71-
result = runner.invoke(run, [entrypoint, "--file", file_path])
77+
result = runner.invoke(run, [script_file_path, "--file", file_path])
7278
assert result.exit_code == 1
73-
assert "Input file extension must be '.json'." in result.output
79+
assert "Invalid Input File Extension" in result.output
7480

7581
def test_run_input_file_success(
7682
self,
@@ -100,7 +106,14 @@ def test_run_input_file_success(
100106
assert "Successful execution." in result.output
101107
assert mock_middleware.call_count == 1
102108
assert mock_middleware.call_args == mock.call(
103-
"run", entrypoint, json_content, False
109+
"run",
110+
entrypoint,
111+
"{}",
112+
False,
113+
debug=False,
114+
debug_port=5678,
115+
input_file=file_path,
116+
execution_output_file=None,
104117
)
105118

106119
class TestMiddleware:
@@ -134,6 +147,7 @@ def test_successful_execution(
134147
simple_script: str,
135148
):
136149
input_file_name = "input.json"
150+
output_file_name = "output.json"
137151
input_json_content = """
138152
{
139153
"message": "Hello world",
@@ -142,6 +156,7 @@ def test_successful_execution(
142156
with runner.isolated_filesystem(temp_dir=temp_dir):
143157
# create input file
144158
input_file_path = os.path.join(temp_dir, input_file_name)
159+
output_file_path = os.path.join(temp_dir, output_file_name)
145160
with open(input_file_path, "w") as f:
146161
f.write(input_json_content)
147162
# Create test script
@@ -152,11 +167,22 @@ def test_successful_execution(
152167
with open("uipath.json", "w") as f:
153168
f.write(uipath_json.to_json())
154169
result = runner.invoke(
155-
run, [script_file_path, "--file", input_file_path]
170+
run,
171+
[
172+
script_file_path,
173+
"--input-file",
174+
input_file_path,
175+
"--output-file",
176+
output_file_path,
177+
],
156178
)
157179
assert result.exit_code == 0
158180
assert "Successful execution." in result.output
159181
assert result.output.count("Hello world") == 2
182+
assert os.path.exists(output_file_path)
183+
with open(output_file_path, "r") as f:
184+
output = f.read()
185+
assert output.count("Hello world") == 2
160186

161187
def test_no_main_function_found(
162188
self,

0 commit comments

Comments
 (0)