Skip to content

Commit 7bd1425

Browse files
feat(cli): camelCase the IPC wire DTOs (#1909)
Every other handler <-> runtime channel (JS result DTO, both runtimes' config files, output.json) is camelCase; the IPC dataclasses were the odd ones out. The handler reads either spelling and selects this contract by uipath version, 2.14.25 and up.
1 parent 6443e4d commit 7bd1425

7 files changed

Lines changed: 179 additions & 189 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.14.24"
3+
version = "2.14.25"
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/_job_api.py‎

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -58,42 +58,38 @@ class ExecutorJobStatus(IntEnum):
5858
class PythonJobLogDto:
5959
"""A log entry; field names are the wire keys (do not rename)."""
6060

61-
JobKey: str
62-
ResumeVersion: int | None = None
63-
Message: str = ""
64-
LogLevel: int = LogLevel.INFORMATION.value
61+
jobKey: str
62+
resumeVersion: int | None = None
63+
message: str = ""
64+
logLevel: int = LogLevel.INFORMATION.value
6565

6666

6767
@dataclass
6868
class JobExecutorError:
6969
"""A result error; field names are the wire keys (do not rename)."""
7070

71-
Code: str | None = None
72-
Title: str | None = None
73-
Detail: str | None = None
74-
Category: str | None = None
75-
Status: int | None = None
71+
code: str | None = None
72+
title: str | None = None
73+
detail: str | None = None
74+
category: str | None = None
75+
status: int | None = None
7676

7777

7878
@dataclass
7979
class PythonJobResultDto:
8080
"""The final result; field names are the wire keys (do not rename)."""
8181

82-
JobKey: str
83-
ResumeVersion: int | None = None
84-
Status: int = ExecutorJobStatus.SUCCESSFUL.value
85-
OutputArguments: Any = None
86-
OutputArgumentsFilePath: str | None = None
87-
Info: str | None = None
88-
Error: JobExecutorError | None = None
82+
jobKey: str
83+
resumeVersion: int | None = None
84+
status: int = ExecutorJobStatus.SUCCESSFUL.value
85+
outputArguments: Any = None
86+
outputArgumentsFilePath: str | None = None
87+
info: str | None = None
88+
error: JobExecutorError | None = None
8989

9090

9191
class IPythonJobApi(ABC):
92-
"""The Python job-api contract: logs + the final result. The class name is the endpoint key.
93-
94-
Every message names the run it belongs to (job key + resume version), so the peer can route a
95-
pooled callback to the right job and drop a straggler from a previous resume.
96-
"""
92+
"""The Python job-api contract: logs + the final result. The class name is the endpoint key."""
9793

9894
@abstractmethod
9995
async def SendLog(self, log: PythonJobLogDto) -> None:
@@ -137,20 +133,20 @@ def _to_result_dto(
137133
if result is not None and getattr(result, "error", None) is not None:
138134
category = result.error.category
139135
error = JobExecutorError(
140-
Code=result.error.code,
141-
Title=result.error.title,
142-
Detail=result.error.detail,
143-
Category=getattr(category, "value", category),
144-
Status=result.error.status,
136+
code=result.error.code,
137+
title=result.error.title,
138+
detail=result.error.detail,
139+
category=getattr(category, "value", category),
140+
status=result.error.status,
145141
)
146142
raw_status = getattr(result, "status", None)
147143
status_key = str(getattr(raw_status, "value", raw_status) or "successful").lower()
148144
return PythonJobResultDto(
149-
JobKey=job_key,
150-
ResumeVersion=resume_version,
151-
Status=_EXECUTOR_STATUS.get(status_key, ExecutorJobStatus.SUCCESSFUL.value),
152-
OutputArgumentsFilePath=output_arguments_file_path,
153-
Error=error,
145+
jobKey=job_key,
146+
resumeVersion=resume_version,
147+
status=_EXECUTOR_STATUS.get(status_key, ExecutorJobStatus.SUCCESSFUL.value),
148+
outputArgumentsFilePath=output_arguments_file_path,
149+
error=error,
154150
)
155151

156152

@@ -194,10 +190,10 @@ def emit(self, record: logging.LogRecord) -> None:
194190
return
195191
try:
196192
dto = PythonJobLogDto(
197-
JobKey=self._job_key,
198-
ResumeVersion=self._resume_version,
199-
Message=self.format(record),
200-
LogLevel=_to_log_level(record.levelno),
193+
jobKey=self._job_key,
194+
resumeVersion=self._resume_version,
195+
message=self.format(record),
196+
logLevel=_to_log_level(record.levelno),
201197
)
202198
future = asyncio.run_coroutine_threadsafe(
203199
self._callback.SendLog(dto), self._loop
@@ -250,10 +246,7 @@ def install_runtime_sinks(
250246
callback: Any,
251247
loop: asyncio.AbstractEventLoop,
252248
) -> "_IpcLogHandler | None":
253-
"""Install the log + result sinks for the run ``(job_key, resume_version)``, forwarding to ``callback`` on ``loop``.
254-
255-
The peer routes a pooled callback by that pair exactly, so ``resume_version`` is the caller's
256-
decision: the value it was handed, or ``None`` on a lane that has none.
249+
"""Install the log + result sinks, forwarding to ``callback`` on ``loop``.
257250
258251
``loop`` must run on a different thread than the one the sinks are invoked on, or the result ack
259252
deadlocks. Raises if this runtime has no sinks to install into.

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,6 @@ async def execute() -> None:
231231
from ._job_api import handler_ipc_connection
232232

233233
async with (
234-
# No resume version reaches this lane; the per-job pipe already names the run.
235234
handler_ipc_connection(handler_ipc_pipe, ctx.job_id, None),
236235
ResourceOverwritesContext(
237236
lambda: read_resource_overwrites_from_file(ctx.runtime_dir)

‎packages/uipath/src/uipath/_cli/cli_server_ipc.py‎

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,30 +25,30 @@ def _run_id(job_key: str, resume_version: int | None) -> str:
2525

2626
@dataclass
2727
class PythonServerRunRequest:
28-
"""PascalCase fields match the wire keys."""
28+
"""camelCase fields match the wire keys."""
2929

30-
JobKey: str = ""
31-
ResumeVersion: int | None = None
32-
Command: str = ""
30+
jobKey: str = ""
31+
resumeVersion: int | None = None
32+
command: str = ""
3333
# The peer sends a single string; HTTP callers and tests may pass a
3434
# pre-split list. parse_args accepts both.
35-
Args: str | list[str] | None = None
36-
WorkingDirectory: str | None = None
37-
EnvironmentVariables: dict[str, str] = field(default_factory=dict)
38-
StreamOutputOverIpc: bool = False
35+
args: str | list[str] | None = None
36+
workingDirectory: str | None = None
37+
environmentVariables: dict[str, str] = field(default_factory=dict)
38+
streamOutputOverIpc: bool = False
3939

4040

4141
@dataclass
4242
class PythonServerStopJobRequest:
43-
JobKey: str = ""
44-
ResumeVersion: int | None = None
45-
ForceStop: bool = False
43+
jobKey: str = ""
44+
resumeVersion: int | None = None
45+
forceStop: bool = False
4646

4747

4848
@dataclass
4949
class PythonServerRunJobResult:
50-
ExitCode: int = 0
51-
Error: str | None = None
50+
exitCode: int = 0
51+
error: str | None = None
5252

5353

5454
class IPythonRuntimeServer(ABC):
@@ -62,7 +62,7 @@ async def Register(self, message: "Message[None]") -> bool:
6262
async def RunJob(
6363
self, request: PythonServerRunRequest, *, message: "Message[None] | None" = None
6464
) -> PythonServerRunJobResult:
65-
"""Run a job → PythonServerRunJobResult(ExitCode, Error).
65+
"""Run a job → PythonServerRunJobResult(exitCode, error).
6666
6767
``message`` is injected by the dispatcher, which reads this contract — not the impl.
6868
"""
@@ -82,28 +82,28 @@ async def Register(self, message: "Message[None]") -> bool:
8282
async def RunJob(
8383
self, request: PythonServerRunRequest, *, message: "Message[None] | None" = None
8484
) -> PythonServerRunJobResult:
85-
command_name = request.Command
85+
command_name = request.command
8686
if not isinstance(command_name, str) or not command_name:
8787
return PythonServerRunJobResult(
88-
ExitCode=1, Error="Missing or invalid field: 'Command'"
88+
exitCode=1, error="Missing or invalid field: 'command'"
8989
)
9090

9191
cmd = COMMANDS.get(command_name)
9292
if cmd is None:
9393
return PythonServerRunJobResult(
94-
ExitCode=1, Error=f"Unknown command: {command_name}"
94+
exitCode=1, error=f"Unknown command: {command_name}"
9595
)
9696

97-
args = parse_args(request.Args)
97+
args = parse_args(request.args)
9898

9999
console.info(
100-
f"Running job {_run_id(request.JobKey, request.ResumeVersion)}: {command_name} {args}"
100+
f"Running job {_run_id(request.jobKey, request.resumeVersion)}: {command_name} {args}"
101101
)
102102

103103
on_run_start: "Any" = None
104104
on_run_end: "Any" = None
105105
installed: "list[Any]" = []
106-
if request.StreamOutputOverIpc:
106+
if request.streamOutputOverIpc:
107107
# Never stored: a captured callback goes stale on reconnect/restart.
108108
from ._job_api import (
109109
IPythonJobApi,
@@ -112,25 +112,25 @@ async def RunJob(
112112
is_wire_job_key,
113113
)
114114

115-
if not is_wire_job_key(request.JobKey):
115+
if not is_wire_job_key(request.jobKey):
116116
return PythonServerRunJobResult(
117-
ExitCode=1,
118-
Error=f"StreamOutputOverIpc needs a 'JobKey' that is a job key (Guid); got {request.JobKey!r}",
117+
exitCode=1,
118+
error=f"streamOutputOverIpc needs a 'jobKey' that is a job key (Guid); got {request.jobKey!r}",
119119
)
120120

121121
if message is None or message.client is None:
122122
return PythonServerRunJobResult(
123-
ExitCode=1,
124-
Error="StreamOutputOverIpc is only available when RunJob is invoked over IPC",
123+
exitCode=1,
124+
error="streamOutputOverIpc is only available when RunJob is invoked over IPC",
125125
)
126126

127127
# get_callback only wraps the connection, so this cannot tell us whether the peer
128128
# actually hosts the contract; a peer that doesn't shows up as a failing send.
129129
callback = message.client.get_callback(IPythonJobApi) # type: ignore[type-abstract]
130130

131131
loop = asyncio.get_running_loop()
132-
job_key = request.JobKey
133-
resume_version = request.ResumeVersion
132+
job_key = request.jobKey
133+
resume_version = request.resumeVersion
134134

135135
def _install() -> None:
136136
installed.append(
@@ -143,8 +143,8 @@ def _install() -> None:
143143
result = await _run_command_isolated(
144144
cmd,
145145
args,
146-
request.EnvironmentVariables,
147-
request.WorkingDirectory,
146+
request.environmentVariables,
147+
request.workingDirectory,
148148
on_run_start=on_run_start,
149149
on_run_end=on_run_end,
150150
)
@@ -154,15 +154,15 @@ def _install() -> None:
154154
if handler is not None:
155155
await handler.aflush_pending()
156156

157-
# IPC contract (PythonServerRunJobResult) carries only ExitCode + Error.
157+
# IPC contract (PythonServerRunJobResult) carries only exitCode + error.
158158
return PythonServerRunJobResult(
159-
ExitCode=result["ExitCode"], Error=result["Error"]
159+
exitCode=result["ExitCode"], error=result["Error"]
160160
)
161161

162162
async def StopJob(self, request: PythonServerStopJobRequest) -> bool:
163163
console.info(
164-
f"StopJob requested for {_run_id(request.JobKey, request.ResumeVersion)} "
165-
f"(force={request.ForceStop}) (no-op)"
164+
f"StopJob requested for {_run_id(request.jobKey, request.resumeVersion)} "
165+
f"(force={request.forceStop}) (no-op)"
166166
)
167167
return True
168168

0 commit comments

Comments
 (0)