diff --git a/quafu/exceptions/__init__.py b/quafu/exceptions/__init__.py index d86e200..29b4edf 100644 --- a/quafu/exceptions/__init__.py +++ b/quafu/exceptions/__init__.py @@ -1 +1,4 @@ +from .circuit_error import * from .quafu_error import * +from .user_error import * +from .utils import validate_server_resp diff --git a/quafu/users/exceptions.py b/quafu/exceptions/user_error.py similarity index 77% rename from quafu/users/exceptions.py rename to quafu/exceptions/user_error.py index 13f4b29..c257eb8 100644 --- a/quafu/users/exceptions.py +++ b/quafu/exceptions/user_error.py @@ -1,4 +1,4 @@ -from ..exceptions import QuafuError +from .quafu_error import QuafuError class UserError(QuafuError): diff --git a/quafu/exceptions/utils.py b/quafu/exceptions/utils.py new file mode 100644 index 0000000..4ddaaca --- /dev/null +++ b/quafu/exceptions/utils.py @@ -0,0 +1,39 @@ +# (C) Copyright 2023 Beijing Academy of Quantum Information Sciences +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .circuit_error import IndexOutOfRangeError, InvalidParaError, UnsupportedYet +from .quafu_error import CircuitError, CompileError, QuafuError, ServerError +from .user_error import APITokenNotFound, BackendNotAvailable, UserError + + +def validate_server_resp(res): + """Check results returned by backend service""" + + status_code = res["status"] if "status" in res else res["code"] + err_msg = ( + res["message"] + if "message" in res + else res["msg"] + if "msg" in res + else "No error message" + ) + + if status_code in [201, 205, 400]: + raise UserError(err_msg) + if status_code == 5001: + raise CircuitError(err_msg) + if status_code == 5003: + raise ServerError(err_msg) + if status_code == 5004: + raise CompileError(err_msg) diff --git a/quafu/tasks/tasks.py b/quafu/tasks/tasks.py index 0df8c90..52535b5 100644 --- a/quafu/tasks/tasks.py +++ b/quafu/tasks/tasks.py @@ -18,16 +18,15 @@ from urllib import parse import numpy as np -import requests from quafu.circuits.quantum_circuit import QuantumCircuit from quafu.users.userapi import User -from ..exceptions import CircuitError, CompileError, ServerError +from ..exceptions import CircuitError, UserError, validate_server_resp from ..results.results import ExecResult, merge_measure -from ..users.exceptions import UserError +from ..utils.client_wrapper import ClientWrapper -class Task(object): +class Task: """ Class for submitting quantum computation task to the backend. @@ -233,11 +232,12 @@ def send( } data = parse.urlencode(data) data = data.replace("%27", "'") - response = requests.post( + response = ClientWrapper.post( url, headers=headers, data=data ) # type: requests.models.Response # TODO: completing status code checks + # FIXME: Maybe we need to delete below code if not response.ok: logging.warning("Received a non-200 response from the server.\n") if response.status_code == 502: @@ -246,26 +246,19 @@ def send( "If there is persistent failure, please report it on our github page." ) raise UserError("502 Bad Gateway response") + # FIXME: Maybe we need to delete above code + + res_dict = response.json() # type: dict + validate_server_resp(res_dict) + + task_id = res_dict["task_id"] + + if group not in self.submit_history: + self.submit_history[group] = [task_id] else: - res_dict = response.json() # type: dict - quafu_status = res_dict["status"] - if quafu_status in [201, 205]: - raise UserError(res_dict["message"]) - elif quafu_status == 5001: - raise CircuitError(res_dict["message"]) - elif quafu_status == 5003: - raise ServerError(res_dict["message"]) - elif quafu_status == 5004: - raise CompileError(res_dict["message"]) - else: - task_id = res_dict["task_id"] - - if group not in self.submit_history: - self.submit_history[group] = [task_id] - else: - self.submit_history[group].append(task_id) - - return ExecResult(res_dict) + self.submit_history[group].append(task_id) + + return ExecResult(res_dict) def retrieve(self, taskid: str) -> ExecResult: """ @@ -281,7 +274,7 @@ def retrieve(self, taskid: str) -> ExecResult: "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", "api_token": self.user.api_token, } - response = requests.post(url, headers=headers, data=data) + response = ClientWrapper.post(url, headers=headers, data=data) res_dict = response.json() return ExecResult(res_dict) diff --git a/quafu/users/userapi.py b/quafu/users/userapi.py index e3a7af4..7aa29f7 100644 --- a/quafu/users/userapi.py +++ b/quafu/users/userapi.py @@ -15,10 +15,9 @@ import os from typing import Optional -import requests - +from ..exceptions import APITokenNotFound, UserError, validate_server_resp +from ..utils.client_wrapper import ClientWrapper from ..utils.platform import get_homedir -from .exceptions import APITokenNotFound, UserError class User(object): @@ -103,12 +102,10 @@ def _get_backends_info(self): """ headers = {"api_token": self.api_token} url = self.url + self.backends_api - response = requests.post(url=url, headers=headers) + response = ClientWrapper.post(url=url, headers=headers) backends_info = response.json() - if backends_info["status"] == 201: - raise UserError(backends_info["message"]) - else: - return backends_info["data"] + validate_server_resp(backends_info) + return backends_info["data"] def get_available_backends(self, print_info=True): """ diff --git a/quafu/utils/client_wrapper.py b/quafu/utils/client_wrapper.py new file mode 100644 index 0000000..3b8ee13 --- /dev/null +++ b/quafu/utils/client_wrapper.py @@ -0,0 +1,31 @@ +# (C) Copyright 2023 Beijing Academy of Quantum Information Sciences +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests +from requests.exceptions import RequestException + +from ..exceptions import ServerError + + +class ClientWrapper: + @staticmethod + def post(*args, **kwargs): + try: + response = requests.post(*args, **kwargs) + response.raise_for_status() + except RequestException as err: + raise ServerError( + f"Failed to communicate with quafu website, please retry later or submit an issue, err: {err}" + ) from err + return response diff --git a/tests/quafu/tasks/data/fake_backends.json b/tests/quafu/tasks/data/fake_backends.json new file mode 100644 index 0000000..be32065 --- /dev/null +++ b/tests/quafu/tasks/data/fake_backends.json @@ -0,0 +1,169 @@ +{ + "data": [ + { + "QV": 0, + "clops": 0.0, + "priority_qubits": null, + "qubits": 10, + "status": "Offline", + "system_id": 0, + "system_name": "ScQ-P10", + "valid_gates": [ + "cx", + "cz", + "rx", + "ry", + "rz", + "x", + "y", + "z", + "h", + "sx", + "sy", + "id", + "delay", + "barrier", + "cy", + "cnot", + "swap" + ] + }, + { + "QV": 0, + "clops": 0.0, + "priority_qubits": null, + "qubits": 8, + "status": "Maintenance", + "system_id": 1, + "system_name": "ScQ-P18", + "valid_gates": [ + "cx", + "cz", + "rx", + "ry", + "rz", + "x", + "y", + "z", + "h", + "sx", + "sy", + "id", + "delay", + "barrier", + "cy", + "cnot", + "swap" + ] + }, + { + "QV": 0, + "clops": 0.0, + "priority_qubits": "[108, 109, 119, 120, 121, 110, 111, 122, 123]", + "qubits": 136, + "status": "Online", + "system_id": 2, + "system_name": "ScQ-P136", + "valid_gates": [ + "cx", + "cz", + "rx", + "ry", + "rz", + "x", + "y", + "z", + "h", + "delay", + "barrier" + ] + }, + { + "QV": 0, + "clops": 0.0, + "priority_qubits": null, + "qubits": 102, + "status": "Maintenance", + "system_id": 3, + "system_name": "ScQ-P102", + "valid_gates": [ + "cx", + "cz", + "rx", + "ry", + "rz", + "x", + "y", + "z", + "h", + "sx", + "sy", + "id", + "delay", + "barrier", + "cy", + "cnot", + "swap" + ] + }, + { + "QV": 0, + "clops": 0.0, + "priority_qubits": null, + "qubits": 10, + "status": "Maintenance", + "system_id": 4, + "system_name": "ScQ-P10C", + "valid_gates": [ + "cx", + "cz", + "rx", + "ry", + "rz", + "x", + "y", + "z", + "h", + "delay", + "barrier" + ] + }, + { + "QV": 1, + "clops": 1.0, + "priority_qubits": null, + "qubits": 2, + "status": "Offline", + "system_id": 5, + "system_name": "ScQ-XXX", + "valid_gates": [ + "cx", + "cz", + "rx", + "ry", + "rz", + "x", + "y", + "z", + "h", + "delay", + "barrier" + ] + }, + { + "QV": 0, + "clops": 0.0, + "priority_qubits": "[1]", + "qubits": 156, + "status": "None Status", + "system_id": 6, + "system_name": "ScQ-P156", + "valid_gates": [ + "cx" + ] + } + ], + "message": "success", + "ok": true, + "status": 200 +} diff --git a/tests/quafu/tasks/data/fake_task_res.json b/tests/quafu/tasks/data/fake_task_res.json new file mode 100644 index 0000000..293c726 --- /dev/null +++ b/tests/quafu/tasks/data/fake_task_res.json @@ -0,0 +1,9 @@ +{ + "measure": "{108: 0, 109: 1}", + "openqasm": "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[136];\ncreg c[2];\nh q[108];\ncx q[108],q[109];\nbarrier q[108],q[109];\nmeasure q[108] -> c[0];\nmeasure q[109] -> c[1];\n", + "raw": "{\"11\": 851, \"00\": 998, \"10\": 90, \"01\": 61}", + "res": "{\"11\": 851, \"00\": 998, \"10\": 90, \"01\": 61}", + "status": 2, + "task_id": "42FBBE201BA554DB", + "task_name": "" +} diff --git a/tests/quafu/tasks/tasks_test.py b/tests/quafu/tasks/tasks_test.py new file mode 100644 index 0000000..2f762c7 --- /dev/null +++ b/tests/quafu/tasks/tasks_test.py @@ -0,0 +1,102 @@ +# (C) Copyright 2023 Beijing Academy of Quantum Information Sciences +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import unittest +from unittest.mock import patch + +from quafu.backends.backends import Backend +from quafu.exceptions import ServerError +from quafu.exceptions.quafu_error import CircuitError, CompileError +from quafu.exceptions.user_error import UserError + +from quafu import QuantumCircuit, Task, User + +DUMMY_API_TOKEN = "123456" + +DUMMY_BACKENDS = {} +with open("tests/quafu/tasks/data/fake_backends.json", "r") as f: + resp = json.loads(f.read()) + DUMMY_BACKENDS = {info["system_name"]: Backend(info) for info in resp["data"]} + + +DUMMY_TASK_RES_DATA = {} +with open("tests/quafu/tasks/data/fake_task_res.json", "r") as f: + DUMMY_TASK_RES_DATA = json.loads(f.read()) + + +DUMMY_CIRC = QuantumCircuit(2) +DUMMY_CIRC.h(0) +DUMMY_CIRC.cx(0, 1) +DUMMY_CIRC.measure() + + +class MockFailedResponse: + def __init__(self, status: int, msg: str) -> None: + self.ok = True + self.status_code = 200 + self._status = status + self._msg = msg + + def json(self): + return {"status": self._status, "msg": self._msg} + + +# FIXME: maybe no need to use this +class MockSucceededResponse: + def __init__(self) -> None: + self.ok = True + self.status_code = 200 + + def json(self): + return DUMMY_TASK_RES_DATA + + +DUMMY_TASK_RES_FAILED_USER = MockFailedResponse(400, "Dummy user error") +DUMMY_TASK_RES_FAILED_CIRCUIT = MockFailedResponse(5001, "Dummy circuit error") +DUMMY_TASK_RES_FAILED_SERVER = MockFailedResponse(5003, "Dummy server error") +DUMMY_TASK_RES_FAILED_COMPILE = MockFailedResponse(5004, "Dummy compile error") + + +class TestTask(unittest.TestCase): + @patch("quafu.users.userapi.User.get_available_backends") + @patch("quafu.utils.client_wrapper.ClientWrapper.post") + def test_send(self, mock_post, mock_get_available_backends): + mock_get_available_backends.return_value = DUMMY_BACKENDS + user = User(api_token=DUMMY_API_TOKEN) + task = Task(user=user) + + # 1. Requests library throws exception + mock_post.side_effect = ServerError() + with self.assertRaises(ServerError): + task.send(DUMMY_CIRC) + + # 2. Website service set customized status code for some errors + mock_post.side_effect = None + + mock_post.return_value = DUMMY_TASK_RES_FAILED_USER + with self.assertRaisesRegex(UserError, "Dummy user error"): + task.send(DUMMY_CIRC) + + mock_post.return_value = DUMMY_TASK_RES_FAILED_CIRCUIT + with self.assertRaisesRegex(CircuitError, "Dummy circuit error"): + task.send(DUMMY_CIRC) + + mock_post.return_value = DUMMY_TASK_RES_FAILED_SERVER + with self.assertRaisesRegex(ServerError, "Dummy server error"): + task.send(DUMMY_CIRC) + + mock_post.return_value = DUMMY_TASK_RES_FAILED_COMPILE + with self.assertRaisesRegex(CompileError, "Dummy compile error"): + task.send(DUMMY_CIRC)