From fdc5c81f214df22a5f14adf6652650d3285c20b1 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Wed, 23 Sep 2026 01:13:29 +0200 Subject: [PATCH] feat(cli): camelCase the IPC wire DTOs 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. --- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/_cli/_job_api.py | 71 +++++----- packages/uipath/src/uipath/_cli/cli_run.py | 1 - .../uipath/src/uipath/_cli/cli_server_ipc.py | 66 ++++----- packages/uipath/tests/cli/test_job_api.py | 95 +++++++------ packages/uipath/tests/cli/test_server_ipc.py | 131 +++++++++--------- packages/uipath/uv.lock | 2 +- 7 files changed, 179 insertions(+), 189 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 010bff5d9..bb12bfa6f 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -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" diff --git a/packages/uipath/src/uipath/_cli/_job_api.py b/packages/uipath/src/uipath/_cli/_job_api.py index 756a137bc..8777bc3a8 100644 --- a/packages/uipath/src/uipath/_cli/_job_api.py +++ b/packages/uipath/src/uipath/_cli/_job_api.py @@ -58,42 +58,38 @@ class ExecutorJobStatus(IntEnum): 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 + resumeVersion: int | None = None + message: str = "" + logLevel: int = LogLevel.INFORMATION.value @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 + resumeVersion: int | None = None + status: int = ExecutorJobStatus.SUCCESSFUL.value + outputArguments: Any = None + outputArgumentsFilePath: str | None = None + 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: @@ -137,20 +133,20 @@ def _to_result_dto( 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, ) @@ -194,10 +190,10 @@ def emit(self, record: logging.LogRecord) -> None: 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 @@ -250,10 +246,7 @@ def install_runtime_sinks( 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. diff --git a/packages/uipath/src/uipath/_cli/cli_run.py b/packages/uipath/src/uipath/_cli/cli_run.py index 05b0cd350..d98b92653 100644 --- a/packages/uipath/src/uipath/_cli/cli_run.py +++ b/packages/uipath/src/uipath/_cli/cli_run.py @@ -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) diff --git a/packages/uipath/src/uipath/_cli/cli_server_ipc.py b/packages/uipath/src/uipath/_cli/cli_server_ipc.py index ffeb092ae..586680274 100644 --- a/packages/uipath/src/uipath/_cli/cli_server_ipc.py +++ b/packages/uipath/src/uipath/_cli/cli_server_ipc.py @@ -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): @@ -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. """ @@ -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, @@ -112,16 +112,16 @@ 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 @@ -129,8 +129,8 @@ async def RunJob( 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( @@ -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, ) @@ -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 diff --git a/packages/uipath/tests/cli/test_job_api.py b/packages/uipath/tests/cli/test_job_api.py index 9ac708b82..df2608890 100644 --- a/packages/uipath/tests/cli/test_job_api.py +++ b/packages/uipath/tests/cli/test_job_api.py @@ -42,19 +42,19 @@ class _Result: dto = _job_api._to_result_dto("job-1", 3, _Result(), "out.args") - assert (dto.JobKey, dto.ResumeVersion) == ("job-1", 3) - assert dto.Status == _job_api.ExecutorJobStatus.FAULTED.value - assert dto.OutputArgumentsFilePath == "out.args" - assert dto.OutputArguments is None - assert dto.Error is not None + assert (dto.jobKey, dto.resumeVersion) == ("job-1", 3) + assert dto.status == _job_api.ExecutorJobStatus.FAULTED.value + assert dto.outputArgumentsFilePath == "out.args" + assert dto.outputArguments is None + assert dto.error is not None # Every field, so a swapped Title/Detail (a stack trace shown as the error's title in the # job's failure record) cannot pass. assert ( - dto.Error.Code, - dto.Error.Title, - dto.Error.Detail, - dto.Error.Category, - dto.Error.Status, + dto.error.code, + dto.error.title, + dto.error.detail, + dto.error.category, + dto.error.status, ) == ("BOOM", "It broke", "stack", "User", 404) @@ -64,9 +64,9 @@ class _Result: error = None dto = _job_api._to_result_dto("j", None, _Result(), "p.args") - assert dto.Status == _job_api.ExecutorJobStatus.SUCCESSFUL.value - assert dto.ResumeVersion is None - assert dto.Error is None + assert dto.status == _job_api.ExecutorJobStatus.SUCCESSFUL.value + assert dto.resumeVersion is None + assert dto.error is None def test_to_result_dto_maps_suspended(): @@ -79,7 +79,7 @@ class _Result: error = None dto = _job_api._to_result_dto("j", None, _Result(), "p.args") - assert dto.Status == _job_api.ExecutorJobStatus.SUSPENDED.value + assert dto.status == _job_api.ExecutorJobStatus.SUSPENDED.value def test_to_log_level_maps_python_levels_to_wire_values(): @@ -96,38 +96,37 @@ def test_to_log_level_maps_python_levels_to_wire_values(): def test_dto_wire_key_sets_are_pinned(): """Pin each DTO's on-wire JSON keys so an accidental rename is caught on this side. - Guards our half of the wire contract: every DTO is PascalCase, matching the peer's property - names (its base-class camelCase JSON names are matched case-insensitively). + Guards our half of the wire contract: every DTO is camelCase, as the peer declares them. """ serialization = pytest.importorskip("uipath_ipc.wire.serialization") to_wire = serialization.to_wire result_keys = set( to_wire( - _job_api.PythonJobResultDto(JobKey="j", OutputArgumentsFilePath="p.args") + _job_api.PythonJobResultDto(jobKey="j", outputArgumentsFilePath="p.args") ) ) assert result_keys == { - "JobKey", - "ResumeVersion", - "Status", - "OutputArguments", - "OutputArgumentsFilePath", - "Info", - "Error", + "jobKey", + "resumeVersion", + "status", + "outputArguments", + "outputArgumentsFilePath", + "info", + "error", } - assert set(to_wire(_job_api.PythonJobLogDto(JobKey="j", Message="m"))) == { - "JobKey", - "ResumeVersion", - "Message", - "LogLevel", + assert set(to_wire(_job_api.PythonJobLogDto(jobKey="j", message="m"))) == { + "jobKey", + "resumeVersion", + "message", + "logLevel", } - assert set(to_wire(_job_api.JobExecutorError(Code="c"))) == { - "Code", - "Title", - "Detail", - "Category", - "Status", + assert set(to_wire(_job_api.JobExecutorError(code="c"))) == { + "code", + "title", + "detail", + "category", + "status", } @@ -182,9 +181,9 @@ async def scenario() -> None: logging.LogRecord("n", logging.WARNING, "p", 1, "hi %s", ("there",), None) ) await handler.aflush_pending() - assert (logs[0].JobKey, logs[0].ResumeVersion) == ("job-7", 2) - assert logs[0].Message == "hi there" - assert logs[0].LogLevel == _job_api.LogLevel.WARNING.value + assert (logs[0].jobKey, logs[0].resumeVersion) == ("job-7", 2) + assert logs[0].message == "hi there" + assert logs[0].logLevel == _job_api.LogLevel.WARNING.value # The result sink maps the result and calls SetResult, off a worker thread, for the ack. class _Result: @@ -193,8 +192,8 @@ class _Result: sink = captured["sink"] await asyncio.to_thread(sink, _Result(), "out.args") - assert (results[0].JobKey, results[0].ResumeVersion) == ("job-7", 2) - assert results[0].OutputArgumentsFilePath == "out.args" + assert (results[0].jobKey, results[0].resumeVersion) == ("job-7", 2) + assert results[0].outputArgumentsFilePath == "out.args" asyncio.run(scenario()) @@ -255,7 +254,7 @@ async def SendLog(self, dto: Any) -> None: thread.join(timeout=5) loop.close() - assert [dto.Message for dto in sent] == ["job line"] + assert [dto.message for dto in sent] == ["job line"] assert buf.getvalue().count("internal chatter") == 2 @@ -331,7 +330,7 @@ def test_pending_log_sends_are_flushed_before_teardown(monkeypatch): class _Callback: async def SendLog(self, dto: Any) -> None: await asyncio.sleep(0.2) - landed.append(dto.Message) + landed.append(dto.message) class _Client: async def aclose(self) -> None: @@ -679,14 +678,14 @@ async def scenario() -> None: # The log half of the contract, asserted on the wire rather than against a fake. assert "logs" in received, "SendLog never arrived over the pipe" entry = received["logs"][0] - assert (entry.JobKey, entry.ResumeVersion) == (JOB_ID_2, 1) - assert entry.Message == "over ipc" - assert entry.LogLevel == _job_api.LogLevel.WARNING.value + assert (entry.jobKey, entry.resumeVersion) == (JOB_ID_2, 1) + assert entry.message == "over ipc" + assert entry.logLevel == _job_api.LogLevel.WARNING.value assert "result" in received, ( "SetResult never arrived — the result sink deadlocked/timed out" ) dto = received["result"] - assert (dto.JobKey, dto.ResumeVersion) == (JOB_ID_2, 1) - assert dto.OutputArgumentsFilePath == "out.args" - assert dto.Status == _job_api.ExecutorJobStatus.SUCCESSFUL.value + assert (dto.jobKey, dto.resumeVersion) == (JOB_ID_2, 1) + assert dto.outputArgumentsFilePath == "out.args" + assert dto.status == _job_api.ExecutorJobStatus.SUCCESSFUL.value diff --git a/packages/uipath/tests/cli/test_server_ipc.py b/packages/uipath/tests/cli/test_server_ipc.py index 8c57cd3ef..2d7d1b8ae 100644 --- a/packages/uipath/tests/cli/test_server_ipc.py +++ b/packages/uipath/tests/cli/test_server_ipc.py @@ -145,47 +145,47 @@ def test_run_job_success(self, pipe, temp_dir): output_file = os.path.join(temp_dir, "output.json") request = { - "JobKey": "job-123", - "Command": "run", - "Args": ["main", "--input-file", input_file, "--output-file", output_file], - "WorkingDirectory": temp_dir, - "EnvironmentVariables": {}, + "jobKey": "job-123", + "command": "run", + "args": ["main", "--input-file", input_file, "--output-file", output_file], + "workingDirectory": temp_dir, + "environmentVariables": {}, } result = asyncio.run(_with_proxy(pipe, lambda p: p.RunJob(request))) - assert result.ExitCode == 0 - assert result.Error is None + assert result.exitCode == 0 + assert result.error is None assert os.path.exists(output_file) with open(output_file, "r") as f: assert "Hello" in f.read() def test_run_job_unknown_command(self, pipe): - request = {"JobKey": "job-1", "Command": "does_not_exist"} + request = {"jobKey": "job-1", "command": "does_not_exist"} result = asyncio.run(_with_proxy(pipe, lambda p: p.RunJob(request))) - assert result.ExitCode != 0 - assert "Unknown command" in (result.Error or "") + assert result.exitCode != 0 + assert "Unknown command" in (result.error or "") def test_run_job_missing_command(self, pipe): """Absent/empty Command is rejected before the job core is touched.""" - result = asyncio.run(_with_proxy(pipe, lambda p: p.RunJob({"JobKey": "job-1"}))) - assert result.ExitCode != 0 - assert "Command" in (result.Error or "") + result = asyncio.run(_with_proxy(pipe, lambda p: p.RunJob({"jobKey": "job-1"}))) + assert result.exitCode != 0 + assert "command" in (result.error or "") def test_run_job_accepts_resume_version(self, pipe): request = { - "JobKey": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", - "ResumeVersion": 4, - "Command": "does_not_exist", + "jobKey": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", + "resumeVersion": 4, + "command": "does_not_exist", } result = asyncio.run(_with_proxy(pipe, lambda p: p.RunJob(request))) - assert "Unknown command" in (result.Error or "") + assert "Unknown command" in (result.error or "") def test_stop_job_accepts_resume_version_and_force_stop(self, pipe): request = { - "JobKey": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", - "ResumeVersion": 2, - "ForceStop": True, + "jobKey": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", + "resumeVersion": 2, + "forceStop": True, } result = asyncio.run(_with_proxy(pipe, lambda p: p.StopJob(request))) @@ -195,7 +195,7 @@ def test_stop_job_returns_true(self, pipe): """StopJob is a no-op stub today, but must ack (bool) so the call is awaitable.""" result = asyncio.run( _with_proxy( - pipe, lambda p: p.StopJob({"JobKey": "job-1", "ForceStop": True}) + pipe, lambda p: p.StopJob({"jobKey": "job-1", "forceStop": True}) ) ) assert result is True @@ -229,16 +229,16 @@ def test_env_vars_do_not_leak_between_jobs(self, pipe_with_spy): async def run_two(proxy: Any) -> None: await proxy.RunJob( { - "JobKey": "job-1", - "Command": "spy", - "EnvironmentVariables": {"TEST_VAR_A": "a"}, + "jobKey": "job-1", + "command": "spy", + "environmentVariables": {"TEST_VAR_A": "a"}, } ) await proxy.RunJob( { - "JobKey": "job-2", - "Command": "spy", - "EnvironmentVariables": {"TEST_VAR_B": "b"}, + "jobKey": "job-2", + "command": "spy", + "environmentVariables": {"TEST_VAR_B": "b"}, } ) @@ -289,7 +289,7 @@ async def RunJob( self, request: Any, *, message: Any = None ) -> PythonServerRunJobResult: received.append(request) - return PythonServerRunJobResult(ExitCode=0) + return PythonServerRunJobResult(exitCode=0) async def StopJob(self, request: Any) -> bool: received.append(request) @@ -304,31 +304,31 @@ async def StopJob(self, request: Any) -> bool: async def drive(proxy: Any) -> None: await proxy.RunJob( { - "JobKey": job_key, - "ResumeVersion": 5, - "Command": "run", - "Args": "main --input-file in.json", - "WorkingDirectory": "/tmp/wd", - "EnvironmentVariables": {"A": "1"}, + "jobKey": job_key, + "resumeVersion": 5, + "command": "run", + "args": "main --input-file in.json", + "workingDirectory": "/tmp/wd", + "environmentVariables": {"A": "1"}, } ) await proxy.StopJob( - {"JobKey": job_key, "ResumeVersion": 5, "ForceStop": True} + {"jobKey": job_key, "resumeVersion": 5, "forceStop": True} ) asyncio.run(_with_proxy(pipe, drive)) run_request, stop_request = received - assert run_request.JobKey == job_key - assert run_request.ResumeVersion == 5 - assert run_request.Command == "run" - assert run_request.Args == "main --input-file in.json" - assert run_request.WorkingDirectory == "/tmp/wd" - assert run_request.EnvironmentVariables == {"A": "1"} + assert run_request.jobKey == job_key + assert run_request.resumeVersion == 5 + assert run_request.command == "run" + assert run_request.args == "main --input-file in.json" + assert run_request.workingDirectory == "/tmp/wd" + assert run_request.environmentVariables == {"A": "1"} - assert stop_request.JobKey == job_key - assert stop_request.ResumeVersion == 5 - assert stop_request.ForceStop is True + assert stop_request.jobKey == job_key + assert stop_request.resumeVersion == 5 + assert stop_request.forceStop is True class TestPooledSinks: @@ -368,15 +368,15 @@ async def _fake_run(*args: Any, **kwargs: Any) -> dict[str, Any]: service = PythonRuntimeService() request = PythonServerRunRequest( - JobKey=JOB_ID, Command="run", Args=[], StreamOutputOverIpc=True + jobKey=JOB_ID, command="run", args=[], streamOutputOverIpc=True ) async def scenario() -> Any: return await service.RunJob(request, message=message) result = asyncio.run(scenario()) - assert result.ExitCode == 1 - assert "over IPC" in (result.Error or "") + assert result.exitCode == 1 + assert "over IPC" in (result.error or "") assert ran == [] @pytest.mark.parametrize( @@ -408,7 +408,7 @@ def get_callback(self, contract: Any) -> Any: service = PythonRuntimeService() request = PythonServerRunRequest( - JobKey=job_key, Command="run", Args=[], StreamOutputOverIpc=True + jobKey=job_key, command="run", args=[], streamOutputOverIpc=True ) async def scenario() -> Any: @@ -417,8 +417,8 @@ async def scenario() -> Any: ) result = asyncio.run(scenario()) - assert result.ExitCode == 1 - assert "JobKey" in (result.Error or "") + assert result.exitCode == 1 + assert "jobKey" in (result.error or "") assert events == [] def test_runjob_without_streaming_does_not_need_a_job_key(self, monkeypatch): @@ -437,13 +437,13 @@ async def _fake_run(*args: Any, **kwargs: Any) -> dict[str, Any]: monkeypatch.setattr(cli_server_ipc, "_run_command_isolated", _fake_run) service = PythonRuntimeService() - request = PythonServerRunRequest(JobKey="", Command="run", Args=[]) + request = PythonServerRunRequest(jobKey="", command="run", args=[]) async def scenario() -> Any: return await service.RunJob(request) result = asyncio.run(scenario()) - assert result.ExitCode == 0 + assert result.exitCode == 0 assert ran == [True] def test_pooled_streaming_works_over_a_real_pipe(self, monkeypatch): @@ -487,7 +487,7 @@ async def _fake_run(cmd, args, env, wd, on_run_start=None, on_run_end=None): class _Callback: async def SendLog(self, log: Any) -> None: - delivered.append((log.JobKey, log)) + delivered.append((log.jobKey, log)) async def SetResult(self, result: Any) -> bool: return True @@ -506,11 +506,11 @@ async def scenario() -> Any: cast( Any, { - "JobKey": JOB_ID, - "ResumeVersion": 2, - "Command": "run", - "Args": [], - "StreamOutputOverIpc": True, + "jobKey": JOB_ID, + "resumeVersion": 2, + "command": "run", + "args": [], + "streamOutputOverIpc": True, }, ) ) @@ -518,15 +518,14 @@ async def scenario() -> Any: await client.aclose() result = asyncio.run(scenario()) - assert result.Error is None, result.Error - assert result.ExitCode == 0 + assert result.error is None, result.error + assert result.exitCode == 0 assert installed == [(JOB_ID, 2)] # The contract passed to get_callback IS the endpoint key on the wire: ask for the wrong # one and every send is addressed to something the peer does not host. assert len(delivered) == 1, "no log line crossed the pooled callback" - # The peer routes by (JobKey, ResumeVersion) exactly; a null here would miss a resumed job. - assert (delivered[0][1].JobKey, delivered[0][1].ResumeVersion) == (JOB_ID, 2) - assert delivered[0][1].Message == "pooled line" + assert (delivered[0][1].jobKey, delivered[0][1].resumeVersion) == (JOB_ID, 2) + assert delivered[0][1].message == "pooled line" def test_runjob_installs_the_sinks_from_the_request_callback(self, monkeypatch): from uipath._cli import _job_api, cli_server_ipc @@ -558,7 +557,7 @@ def get_callback(self, contract: Any) -> Any: service = PythonRuntimeService() request = PythonServerRunRequest( - JobKey=JOB_ID, Command="run", Args=[], StreamOutputOverIpc=True + jobKey=JOB_ID, command="run", args=[], streamOutputOverIpc=True ) async def scenario() -> None: @@ -598,7 +597,7 @@ def get_callback(self, contract: Any) -> Any: service = PythonRuntimeService() request = PythonServerRunRequest( - JobKey=JOB_ID, Command="run", Args=[], StreamOutputOverIpc=True + jobKey=JOB_ID, command="run", args=[], streamOutputOverIpc=True ) async def scenario() -> None: @@ -632,7 +631,7 @@ async def _fake_run(cmd, args, env, wd, on_run_start=None, on_run_end=None): monkeypatch.setattr(cli_server_ipc, "_run_command_isolated", _fake_run) service = PythonRuntimeService() - request = PythonServerRunRequest(JobKey="job-9", Command="run", Args=[]) + request = PythonServerRunRequest(jobKey="job-9", command="run", args=[]) async def scenario() -> None: await service.RunJob(request, message=Message(client=None)) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 371696ba7..bc92233bb 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2599,7 +2599,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.14.24" +version = "2.14.25" source = { editable = "." } dependencies = [ { name = "applicationinsights" },