# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2018.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
# Copyright 2020 IonQ, Inc. (www.ionq.com)
#
# 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.
"""Exceptions for the IonQ Provider."""
from __future__ import annotations
from typing import Literal
import json.decoder as jd
import requests
from qiskit.exceptions import QiskitError
from qiskit.providers import JobError, JobTimeoutError
[docs]
class IonQError(QiskitError):
    """Base class for errors raised by an IonQProvider."""
    def __str__(self) -> str:
        return f"{self.__class__.__name__}({self.message!r})"
    def __repr__(self) -> str:
        return repr(str(self)) 
[docs]
class IonQCredentialsError(IonQError):
    """Errors generated from bad credentials or config""" 
[docs]
class IonQClientError(IonQError):
    """Errors that arise from unexpected behavior while using IonQClient.""" 
class IonQRetriableError(IonQError):
    """Errors that do not indicate a failure related to the request, and can be retried."""
    def __init__(self, cause):
        self._cause = cause
        super().__init__(getattr(cause, "message", "Unknown error"))
# pylint: disable=no-member
# https://support.cloudflare.com/hc/en-us/articles/115003014512-4xx-Client-Error
# "Cloudflare will generate and serve a 409 response for a Error 1001: DNS Resolution Error."
# We may want to condition on the body as well, to allow for some GET requests to return 409 in
# the future.
_RETRIABLE_FOR_GETS = {requests.codes.conflict}
# Retriable regardless of the source
# Handle 52x responses from cloudflare.
# See https://support.cloudflare.com/hc/en-us/articles/115003011431/
_RETRIABLE_STATUS_CODES = {
    requests.codes.internal_server_error,
    requests.codes.bad_gateway,
    requests.codes.service_unavailable,
    *list(range(520, 530)),
}
# pylint: enable=no-member
def _is_retriable(method, code):
    return code in _RETRIABLE_STATUS_CODES or (
        method == "GET" and code in _RETRIABLE_FOR_GETS
    )
[docs]
class IonQAPIError(IonQError):
    """Base exception for fatal API errors.
    Attributes:
        status_code(int): An HTTP response status code.
        error_type(str): An error type string from the IonQ REST API.
    """
    def __init__(self, message, status_code, headers, body, error_type):  # pylint: disable=too-many-positional-arguments
        super().__init__(message)
        self.status_code = status_code
        self.headers = headers
        self.body = body
        self.error_type = error_type
[docs]
    @classmethod
    def raise_for_status(cls, response) -> IonQAPIError | None:
        """Raise an instance of the exception class from an API response object if needed.
        Args:
            response (:class:`Response <requests.Response>`): An IonQ REST API response.
        Raises:
            IonQAPIError: instance of `cls` with error detail from `response`.
            IonQRetriableError:  instance of `cls` with error detail from `response`."""
        status_code = response.status_code
        if status_code < 400:
            # No error, return None
            return None
        res = cls.from_response(response)
        if _is_retriable(response.request.method, status_code):
            raise IonQRetriableError(res)
        raise res 
[docs]
    @classmethod
    def from_response(cls, response: requests.Response) -> IonQAPIError:
        """Raise an instance of the exception class from an API response object.
        Args:
            response (:class:`Response <requests.Response>`): An IonQ REST API response.
        Returns:
            IonQAPIError: instance of `cls` with error detail from `response`.
        """
        # TODO: Pending API changes will cleanup this error logic:
        status_code = response.status_code
        headers = response.headers
        body = response.text
        try:
            response_json = response.json()
        except jd.JSONDecodeError:
            response_json = {"invalid_json": response.text}
        # Defaults, if items cannot be extracted from the response.
        error_type = "internal_error"
        message = "No error details provided."
        if "code" in response_json:
            # { "code": <int>, "message": <str> }
            message = response_json.get("message") or message
        elif "statusCode" in response_json:
            # { "statusCode": <int>, "error": <str>, "message": <str> }
            message = response_json.get("message") or message
            error_type = response_json.get("error") or error_type
        elif "error" in response_json:
            # { "error": { "type": <str>, "message: <str> } }
            error_data = response_json.get("error")
            message = error_data.get("message") or message
            error_type = error_data.get("type") or error_type
        return cls(message, status_code, headers, body, error_type) 
    def __str__(self):
        return (
            f"{self.__class__.__name__}("
            f"message={self.message!r},"
            f"status_code={self.status_code!r},"
            f"headers={self.headers},"
            f"body={self.body},"
            f"error_type={self.error_type!r})"
        )
    def __reduce__(self):
        return (
            self.__class__,
            (self.message, self.status_code, self.headers, self.body, self.error_type),
        ) 
[docs]
class IonQBackendError(IonQError):
    """Errors generated from improper usage of IonQBackend objects.""" 
[docs]
class IonQJobError(IonQError, JobError):
    """Errors generated from improper usage of IonQJob objects.""" 
class IonQJobFailureError(IonQError, JobError):
    """Errors generated from jobs that fail on the API side."""
class IonQJobStateError(IonQError, JobError):
    """Errors generated from attempting to do something to a job that its state would not allow"""
[docs]
class IonQGateError(IonQError, JobError):
    """Errors generated from invalid gate defs
    Attributes:
        gate_name: The name of the gate which caused this error.
    """
    def __init__(self, gate_name: str, gateset: Literal["qis", "native"]):
        self.gate_name = gate_name
        self.gateset = gateset
        super().__init__(
            (
                f"gate '{gate_name}' is not supported on the '{gateset}' IonQ backends. "
                "Please use the qiskit.transpile method, manually rewrite to remove the gate, "
                "or change the gateset selection as appropriate."
            )
        )
    def __repr__(self):
        return f"{self.__class__.__name__}(gate_name={self.gate_name!r}, gateset={self.gateset!r})" 
[docs]
class IonQMidCircuitMeasurementError(IonQError, JobError):
    """Errors generated from attempting mid-circuit measurement, which is not supported.
    Measurement must come after all instructions.
    Attributes:
        qubit_index: The qubit index to be measured mid-circuit
    """
    def __init__(self, qubit_index: int, gate_name: str):
        self.qubit_index = qubit_index
        self.gate_name = gate_name
        super().__init__(
            f"Attempting to put '{gate_name}' after a measurement on qubit {qubit_index}. "
            "Mid-circuit measurement is not supported."
        )
    def __str__(self):
        kwargs = f"qubit_index={self.qubit_index!r}, gate_name={self.gate_name!r}"
        return f"{self.__class__.__name__}({kwargs})" 
[docs]
class IonQJobTimeoutError(IonQError, JobTimeoutError):
    """Errors generated from job timeouts""" 
[docs]
class IonQPauliExponentialError(IonQError):
    """Errors generated from improper usage of Pauli exponentials.""" 
[docs]
class IonQTranspileLevelWarning(UserWarning):
    """Transpiler optimization level may degrade IonQ QIS results.""" 
__all__ = [
    "IonQError",
    "IonQCredentialsError",
    "IonQClientError",
    "IonQAPIError",
    "IonQBackendError",
    "IonQJobError",
    "IonQGateError",
    "IonQMidCircuitMeasurementError",
    "IonQJobTimeoutError",
    "IonQPauliExponentialError",
    "IonQTranspileLevelWarning",
]