From 905d0174ee7aac5c88033163e27e6c0bcdf8f90b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olaf=20Sch=C3=BCsler?= Date: Fri, 24 Jan 2025 14:46:42 +0100 Subject: [PATCH] [QI2-1315] Add error retrieval --- poetry.lock | 8 +++--- pyproject.toml | 2 +- quantuminspire/cli/command_list.py | 18 ++++++++++-- quantuminspire/util/api/remote_backend.py | 8 ++++++ tests/cli/test_command_list.py | 35 +++++++++++++++++++++++ tests/util/api/test_remote_backend.py | 11 +++++++ 6 files changed, 75 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0e8affa0..1e0e01ba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1491,13 +1491,13 @@ files = [ [[package]] name = "qi-compute-api-client" -version = "0.44.0" +version = "0.45.0" description = "An API client for the Compute Job Manager of Quantum Inspire." optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "qi_compute_api_client-0.44.0-py3-none-any.whl", hash = "sha256:e16ad15345d142981b9ae95104cc35b1a5d00281e32e64a034f88b8987091877"}, - {file = "qi_compute_api_client-0.44.0.tar.gz", hash = "sha256:15f690a79d19c6f1ec044098151be0658a0c0c98d4b23af51aec00dc707d7d34"}, + {file = "qi_compute_api_client-0.45.0-py3-none-any.whl", hash = "sha256:d10fa4a4f75ab00df31d8ec9e036ee775744f66068d55299a60116a519ef02a9"}, + {file = "qi_compute_api_client-0.45.0.tar.gz", hash = "sha256:7fa88cdfe707e468169d3aeb4d3e3587809ec8ec0340a2018c6d5b36310b0be4"}, ] [package.dependencies] @@ -2069,4 +2069,4 @@ local = ["qxelarator"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "2cf8b1a64fe1c2b4fe7ed8f4cb41547a5c32ac4a29f5bdeb908c462eda07c73b" +content-hash = "44630a07c2476ca30770bc51049d7e51987e91c78d06a28dd2e3e6bc6b85f17d" diff --git a/pyproject.toml b/pyproject.toml index 38379c1f..fcdc44f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ qi = "quantuminspire.cli.command_list:app" python = "^3.9" typer = {extras = ["all"], version = "^0.15.1"} pydantic = "^2.10.6" -qi-compute-api-client = "^0.44.0" +qi-compute-api-client = "^0.45.0" qxelarator = {version = "^0.7.2", optional = true} pydantic-settings = "^2.7.1" qiskit = "1.0.2" diff --git a/quantuminspire/cli/command_list.py b/quantuminspire/cli/command_list.py index 757554f8..07c33cd7 100644 --- a/quantuminspire/cli/command_list.py +++ b/quantuminspire/cli/command_list.py @@ -7,6 +7,8 @@ from typing import Optional import typer +from compute_api_client import JobStatus +from rich import print from typer import Typer from quantuminspire.sdk.models.cqasm_algorithm import CqasmAlgorithm @@ -298,6 +300,16 @@ def run_file( typer.echo(f"{results}") +def _has_job_failed(backend: RemoteBackend, job_id: int) -> None: + """Check if a job has failed and exit with error_code 1 if it has.""" + job = backend.get_job(job_id) + if job.status == JobStatus.FAILED: + job.message = job.message or "Job failed." + typer.echo(job.message, err=True) + typer.echo(f"Trace id: {job.trace_id}", err=True) + raise typer.Exit(1) + + @results_app.command("get") def get_results(job_id: int = typer.Argument(..., help="The id of the run")) -> None: """Retrieve the results for a run. @@ -305,6 +317,7 @@ def get_results(job_id: int = typer.Argument(..., help="The id of the run")) -> Takes the id as returned by upload_files and retrieves the results for that run, if it's finished. """ backend = RemoteBackend() + _has_job_failed(backend, job_id) results = backend.get_results(job_id) if results is None: @@ -312,7 +325,7 @@ def get_results(job_id: int = typer.Argument(..., help="The id of the run")) -> raise typer.Exit(1) typer.echo("Raw results:") - typer.echo(results) + print(results.model_dump()) @final_results_app.command("get") @@ -322,6 +335,7 @@ def get_final_results(job_id: int = typer.Argument(..., help="The id of the run" Takes the id as returned by upload_files and retrieves the final results for that job, if it's finished. """ backend = RemoteBackend() + _has_job_failed(backend, job_id) results = backend.get_final_results(job_id) if results is None: @@ -329,7 +343,7 @@ def get_final_results(job_id: int = typer.Argument(..., help="The id of the run" raise typer.Exit(1) typer.echo("Raw final results:") - typer.echo(results) + print(results.model_dump()) @app.command("login") diff --git a/quantuminspire/util/api/remote_backend.py b/quantuminspire/util/api/remote_backend.py index 29ae665b..c54f8f5a 100644 --- a/quantuminspire/util/api/remote_backend.py +++ b/quantuminspire/util/api/remote_backend.py @@ -79,6 +79,10 @@ def run( """Execute provided algorithm/circuit.""" return asyncio.run(self._create_flow(program, backend_type_id, job_options=options)) + async def _get_job(self, job_id: int) -> Any: + async with ApiClient(self._configuration) as api_client: + return await self._read_job(api_client, job_id) + async def _get_results(self, job_id: int) -> Any: async with ApiClient(self._configuration) as api_client: job = await self._read_job(api_client, job_id) @@ -95,6 +99,10 @@ async def _get_final_results(self, job_id: int) -> Any: return await self._read_final_results_for_job(api_client, job) + def get_job(self, job_id: int) -> Any: + """Get job for algorithm/circuit.""" + return asyncio.run(self._get_job(job_id)) + def get_results(self, job_id: int) -> Any: """Get results for algorithm/circuit.""" return asyncio.run(self._get_results(job_id)) diff --git a/tests/cli/test_command_list.py b/tests/cli/test_command_list.py index e0030bb9..6dd38267 100644 --- a/tests/cli/test_command_list.py +++ b/tests/cli/test_command_list.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock import pytest +from compute_api_client import JobStatus from pytest_mock import MockerFixture from typer.testing import CliRunner @@ -109,6 +110,24 @@ def test_results_get(mocker: MockerFixture) -> None: mock_remote_backend_inst.get_results.assert_called_once() +def test_results_get_failed_job(mocker: MockerFixture) -> None: + mock_remote_backend_inst = MagicMock() + job = MagicMock() + job.id = 1 + job.status = JobStatus.FAILED + job.message = "Job failed." + job.trace_id = "trace_id" + mock_remote_backend_inst.get_job.return_value = job + mocker.patch("quantuminspire.cli.command_list.RemoteBackend", return_value=mock_remote_backend_inst) + + result = runner.invoke(app, ["results", "get", "1"]) + + print(result.stdout) + + assert result.stdout == "Job failed.\nTrace id: trace_id\n" + assert result.exit_code == 1 + + def test_results_get_no_results(mocker: MockerFixture) -> None: mock_remote_backend_inst = MagicMock() mock_remote_backend_inst.get_results.return_value = None @@ -130,6 +149,22 @@ def test_final_results_get(mocker: MockerFixture) -> None: mock_remote_backend_inst.get_final_results.assert_called_once() +def test_final_results_get_failed_job(mocker: MockerFixture) -> None: + mock_remote_backend_inst = MagicMock() + job = MagicMock() + job.id = 1 + job.status = JobStatus.FAILED + job.message = "Job failed." + job.trace_id = "trace_id" + mock_remote_backend_inst.get_job.return_value = job + mocker.patch("quantuminspire.cli.command_list.RemoteBackend", return_value=mock_remote_backend_inst) + + result = runner.invoke(app, ["final_results", "get", "1"]) + + assert result.stdout == "Job failed.\nTrace id: trace_id\n" + assert result.exit_code == 1 + + def test_final_results_get_no_results(mocker: MockerFixture) -> None: mock_remote_backend_inst = MagicMock() mock_remote_backend_inst.get_final_results.return_value = None diff --git a/tests/util/api/test_remote_backend.py b/tests/util/api/test_remote_backend.py index b7db0c38..0965dcf4 100644 --- a/tests/util/api/test_remote_backend.py +++ b/tests/util/api/test_remote_backend.py @@ -84,6 +84,17 @@ def test_run( api_client.assert_has_calls([call().__aenter__(), call().__aexit__(None, None, None)]) +def test_get_job( + mocker: MockerFixture, api_client: MagicMock, mocked_settings: MagicMock, mocked_authentication: MagicMock +) -> None: + backend = RemoteBackend() + jobs_api_instance = AsyncMock() + job_id = 1 + mocker.patch("quantuminspire.util.api.remote_backend.JobsApi", return_value=jobs_api_instance) + backend.get_job(job_id) + jobs_api_instance.read_job_jobs_id_get.assert_called_with(job_id) + + def test_get_results( mocker: MockerFixture, api_client: MagicMock, mocked_settings: MagicMock, mocked_authentication: MagicMock ) -> None: