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
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.14.24"
version = "2.14.25"
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
71 changes: 32 additions & 39 deletions packages/uipath/src/uipath/_cli/_job_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,42 +58,38 @@
class PythonJobLogDto:
"""A log entry; field names are the wire keys (do not rename)."""

JobKey: str
ResumeVersion: int | None = None
Message: str = ""
LogLevel: int = LogLevel.INFORMATION.value
jobKey: str

Check warning on line 61 in packages/uipath/src/uipath/_cli/_job_api.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this field "jobKey" to match the regular expression ^[_a-z][_a-z0-9]*$.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AaDLb14n8ZdbM6wMXa9b&open=AaDLb14n8ZdbM6wMXa9b&pullRequest=1909
resumeVersion: int | None = None

Check warning on line 62 in packages/uipath/src/uipath/_cli/_job_api.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this field "resumeVersion" to match the regular expression ^[_a-z][_a-z0-9]*$.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AaDLb14n8ZdbM6wMXa9a&open=AaDLb14n8ZdbM6wMXa9a&pullRequest=1909
message: str = ""
logLevel: int = LogLevel.INFORMATION.value

Check warning on line 64 in packages/uipath/src/uipath/_cli/_job_api.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this field "logLevel" to match the regular expression ^[_a-z][_a-z0-9]*$.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AaDLb14n8ZdbM6wMXa9Z&open=AaDLb14n8ZdbM6wMXa9Z&pullRequest=1909


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

Code: str | None = None
Title: str | None = None
Detail: str | None = None
Category: str | None = None
Status: int | None = None
code: str | None = None
title: str | None = None
detail: str | None = None
category: str | None = None
status: int | None = None


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

JobKey: str
ResumeVersion: int | None = None
Status: int = ExecutorJobStatus.SUCCESSFUL.value
OutputArguments: Any = None
OutputArgumentsFilePath: str | None = None
Info: str | None = None
Error: JobExecutorError | None = None
jobKey: str

Check warning on line 82 in packages/uipath/src/uipath/_cli/_job_api.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this field "jobKey" to match the regular expression ^[_a-z][_a-z0-9]*$.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AaDLb14n8ZdbM6wMXa9d&open=AaDLb14n8ZdbM6wMXa9d&pullRequest=1909
resumeVersion: int | None = None

Check warning on line 83 in packages/uipath/src/uipath/_cli/_job_api.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this field "resumeVersion" to match the regular expression ^[_a-z][_a-z0-9]*$.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AaDLb14n8ZdbM6wMXa9c&open=AaDLb14n8ZdbM6wMXa9c&pullRequest=1909
status: int = ExecutorJobStatus.SUCCESSFUL.value
outputArguments: Any = None

Check warning on line 85 in packages/uipath/src/uipath/_cli/_job_api.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this field "outputArguments" to match the regular expression ^[_a-z][_a-z0-9]*$.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AaDLb14n8ZdbM6wMXa9f&open=AaDLb14n8ZdbM6wMXa9f&pullRequest=1909
outputArgumentsFilePath: str | None = None

Check warning on line 86 in packages/uipath/src/uipath/_cli/_job_api.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this field "outputArgumentsFilePath" to match the regular expression ^[_a-z][_a-z0-9]*$.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AaDLb14n8ZdbM6wMXa9e&open=AaDLb14n8ZdbM6wMXa9e&pullRequest=1909
info: str | None = None
error: JobExecutorError | None = None


class IPythonJobApi(ABC):
"""The Python job-api contract: logs + the final result. The class name is the endpoint key.

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

@abstractmethod
async def SendLog(self, log: PythonJobLogDto) -> None:
Expand Down Expand Up @@ -137,20 +133,20 @@
if result is not None and getattr(result, "error", None) is not None:
category = result.error.category
error = JobExecutorError(
Code=result.error.code,
Title=result.error.title,
Detail=result.error.detail,
Category=getattr(category, "value", category),
Status=result.error.status,
code=result.error.code,
title=result.error.title,
detail=result.error.detail,
category=getattr(category, "value", category),
status=result.error.status,
)
raw_status = getattr(result, "status", None)
status_key = str(getattr(raw_status, "value", raw_status) or "successful").lower()
return PythonJobResultDto(
JobKey=job_key,
ResumeVersion=resume_version,
Status=_EXECUTOR_STATUS.get(status_key, ExecutorJobStatus.SUCCESSFUL.value),
OutputArgumentsFilePath=output_arguments_file_path,
Error=error,
jobKey=job_key,
resumeVersion=resume_version,
status=_EXECUTOR_STATUS.get(status_key, ExecutorJobStatus.SUCCESSFUL.value),
outputArgumentsFilePath=output_arguments_file_path,
error=error,
)


Expand Down Expand Up @@ -194,10 +190,10 @@
return
try:
dto = PythonJobLogDto(
JobKey=self._job_key,
ResumeVersion=self._resume_version,
Message=self.format(record),
LogLevel=_to_log_level(record.levelno),
jobKey=self._job_key,
resumeVersion=self._resume_version,
message=self.format(record),
logLevel=_to_log_level(record.levelno),
)
future = asyncio.run_coroutine_threadsafe(
self._callback.SendLog(dto), self._loop
Expand Down Expand Up @@ -250,10 +246,7 @@
callback: Any,
loop: asyncio.AbstractEventLoop,
) -> "_IpcLogHandler | None":
"""Install the log + result sinks for the run ``(job_key, resume_version)``, forwarding to ``callback`` on ``loop``.

The peer routes a pooled callback by that pair exactly, so ``resume_version`` is the caller's
decision: the value it was handed, or ``None`` on a lane that has none.
"""Install the log + result sinks, forwarding to ``callback`` on ``loop``.

``loop`` must run on a different thread than the one the sinks are invoked on, or the result ack
deadlocks. Raises if this runtime has no sinks to install into.
Expand Down
1 change: 0 additions & 1 deletion packages/uipath/src/uipath/_cli/cli_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,6 @@ async def execute() -> None:
from ._job_api import handler_ipc_connection

async with (
# No resume version reaches this lane; the per-job pipe already names the run.
handler_ipc_connection(handler_ipc_pipe, ctx.job_id, None),
ResourceOverwritesContext(
lambda: read_resource_overwrites_from_file(ctx.runtime_dir)
Expand Down
66 changes: 33 additions & 33 deletions packages/uipath/src/uipath/_cli/cli_server_ipc.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,30 @@ def _run_id(job_key: str, resume_version: int | None) -> str:

@dataclass
class PythonServerRunRequest:
"""PascalCase fields match the wire keys."""
"""camelCase fields match the wire keys."""

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


@dataclass
class PythonServerStopJobRequest:
JobKey: str = ""
ResumeVersion: int | None = None
ForceStop: bool = False
jobKey: str = ""
resumeVersion: int | None = None
forceStop: bool = False


@dataclass
class PythonServerRunJobResult:
ExitCode: int = 0
Error: str | None = None
exitCode: int = 0
error: str | None = None


class IPythonRuntimeServer(ABC):
Expand All @@ -62,7 +62,7 @@ async def Register(self, message: "Message[None]") -> bool:
async def RunJob(
self, request: PythonServerRunRequest, *, message: "Message[None] | None" = None
) -> PythonServerRunJobResult:
"""Run a job → PythonServerRunJobResult(ExitCode, Error).
"""Run a job → PythonServerRunJobResult(exitCode, error).

``message`` is injected by the dispatcher, which reads this contract — not the impl.
"""
Expand All @@ -82,28 +82,28 @@ async def Register(self, message: "Message[None]") -> bool:
async def RunJob(
self, request: PythonServerRunRequest, *, message: "Message[None] | None" = None
) -> PythonServerRunJobResult:
command_name = request.Command
command_name = request.command
if not isinstance(command_name, str) or not command_name:
return PythonServerRunJobResult(
ExitCode=1, Error="Missing or invalid field: 'Command'"
exitCode=1, error="Missing or invalid field: 'command'"
)

cmd = COMMANDS.get(command_name)
if cmd is None:
return PythonServerRunJobResult(
ExitCode=1, Error=f"Unknown command: {command_name}"
exitCode=1, error=f"Unknown command: {command_name}"
)

args = parse_args(request.Args)
args = parse_args(request.args)

console.info(
f"Running job {_run_id(request.JobKey, request.ResumeVersion)}: {command_name} {args}"
f"Running job {_run_id(request.jobKey, request.resumeVersion)}: {command_name} {args}"
)

on_run_start: "Any" = None
on_run_end: "Any" = None
installed: "list[Any]" = []
if request.StreamOutputOverIpc:
if request.streamOutputOverIpc:
# Never stored: a captured callback goes stale on reconnect/restart.
from ._job_api import (
IPythonJobApi,
Expand All @@ -112,25 +112,25 @@ async def RunJob(
is_wire_job_key,
)

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

if message is None or message.client is None:
return PythonServerRunJobResult(
ExitCode=1,
Error="StreamOutputOverIpc is only available when RunJob is invoked over IPC",
exitCode=1,
error="streamOutputOverIpc is only available when RunJob is invoked over IPC",
)

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

loop = asyncio.get_running_loop()
job_key = request.JobKey
resume_version = request.ResumeVersion
job_key = request.jobKey
resume_version = request.resumeVersion

def _install() -> None:
installed.append(
Expand All @@ -143,8 +143,8 @@ def _install() -> None:
result = await _run_command_isolated(
cmd,
args,
request.EnvironmentVariables,
request.WorkingDirectory,
request.environmentVariables,
request.workingDirectory,
on_run_start=on_run_start,
on_run_end=on_run_end,
)
Expand All @@ -154,15 +154,15 @@ def _install() -> None:
if handler is not None:
await handler.aflush_pending()

# IPC contract (PythonServerRunJobResult) carries only ExitCode + Error.
# IPC contract (PythonServerRunJobResult) carries only exitCode + error.
return PythonServerRunJobResult(
ExitCode=result["ExitCode"], Error=result["Error"]
exitCode=result["ExitCode"], error=result["Error"]
)

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

Expand Down
Loading
Loading