From f36e01e421ba1f2c8e94ad1c81647c8b27a86c4c Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Fri, 7 Jul 2023 16:08:45 -0400 Subject: [PATCH] Use qiskit-ibm-provider api code (#871) * use qiskit-ibm-provider api code * remove duplicated rest files * remove duplicate clients files * check integration tests * fix metrics typing * fix job inputs * update proxies & auth client tests * re-enable integration tests --- qiskit_ibm_runtime/api/clients/__init__.py | 8 +- qiskit_ibm_runtime/api/clients/auth.py | 174 ---------- qiskit_ibm_runtime/api/clients/backend.py | 2 +- qiskit_ibm_runtime/api/clients/base.py | 306 ------------------ qiskit_ibm_runtime/api/clients/runtime.py | 2 +- qiskit_ibm_runtime/api/clients/runtime_ws.py | 74 ----- qiskit_ibm_runtime/api/clients/version.py | 48 --- qiskit_ibm_runtime/api/rest/__init__.py | 2 +- qiskit_ibm_runtime/api/rest/backend.py | 137 -------- qiskit_ibm_runtime/api/rest/base.py | 45 --- qiskit_ibm_runtime/api/rest/cloud_backend.py | 2 +- qiskit_ibm_runtime/api/rest/program.py | 2 +- qiskit_ibm_runtime/api/rest/program_job.py | 97 ------ qiskit_ibm_runtime/api/rest/root.py | 100 ------ qiskit_ibm_runtime/api/rest/runtime.py | 4 +- .../api/rest/runtime_session.py | 2 +- qiskit_ibm_runtime/runtime_job.py | 8 +- requirements.txt | 2 +- setup.py | 2 +- test/integration/test_auth_client.py | 9 +- test/integration/test_proxies.py | 15 +- 21 files changed, 28 insertions(+), 1013 deletions(-) delete mode 100644 qiskit_ibm_runtime/api/clients/auth.py delete mode 100644 qiskit_ibm_runtime/api/clients/base.py delete mode 100644 qiskit_ibm_runtime/api/clients/runtime_ws.py delete mode 100644 qiskit_ibm_runtime/api/clients/version.py delete mode 100644 qiskit_ibm_runtime/api/rest/backend.py delete mode 100644 qiskit_ibm_runtime/api/rest/base.py delete mode 100644 qiskit_ibm_runtime/api/rest/program_job.py delete mode 100644 qiskit_ibm_runtime/api/rest/root.py diff --git a/qiskit_ibm_runtime/api/clients/__init__.py b/qiskit_ibm_runtime/api/clients/__init__.py index b07d4613c..eba744e56 100644 --- a/qiskit_ibm_runtime/api/clients/__init__.py +++ b/qiskit_ibm_runtime/api/clients/__init__.py @@ -12,8 +12,8 @@ """IBM Quantum API clients.""" -from .base import BaseClient, WebsocketClientCloseCode -from .auth import AuthClient -from .version import VersionClient +from qiskit_ibm_provider.api.clients.base import BaseClient, WebsocketClientCloseCode +from qiskit_ibm_provider.api.clients.auth import AuthClient +from qiskit_ibm_provider.api.clients.version import VersionClient +from qiskit_ibm_provider.api.clients.runtime_ws import RuntimeWebsocketClient from .runtime import RuntimeClient -from .runtime_ws import RuntimeWebsocketClient diff --git a/qiskit_ibm_runtime/api/clients/auth.py b/qiskit_ibm_runtime/api/clients/auth.py deleted file mode 100644 index c16e14299..000000000 --- a/qiskit_ibm_runtime/api/clients/auth.py +++ /dev/null @@ -1,174 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Client for accessing IBM Quantum authentication services.""" - -from typing import Dict, List, Optional, Any, Union -from requests.exceptions import RequestException - -from ..auth import QuantumAuth -from ..exceptions import AuthenticationLicenseError, RequestsApiError -from ..rest import Api -from ..session import RetrySession -from ..client_parameters import ClientParameters - -from .base import BaseClient - - -class AuthClient(BaseClient): - """Client for accessing IBM Quantum authentication services.""" - - def __init__(self, client_params: ClientParameters) -> None: - """AuthClient constructor. - - Args: - client_params: Parameters used for server connection. - """ - self.api_token = client_params.token - self.auth_url = client_params.url - self._service_urls = {} # type: ignore[var-annotated] - - self.auth_api = Api(RetrySession(self.auth_url, **client_params.connection_parameters())) - self.base_api = self._init_service_clients(**client_params.connection_parameters()) - - def _init_service_clients(self, **request_kwargs: Any) -> Api: - """Initialize the clients used for communicating with the API. - - Args: - **request_kwargs: Arguments for the request ``Session``. - - Returns: - Client for the API server. - """ - # Request an access token. - self.access_token = self._request_access_token() - self.auth_api.session.auth = QuantumAuth(access_token=self.access_token) - self._service_urls = self.user_urls() - - # Create the api server client, using the access token. - base_api = Api( - RetrySession( - self._service_urls["http"], - auth=QuantumAuth(access_token=self.access_token), - **request_kwargs, - ) - ) - - return base_api - - def _request_access_token(self) -> str: - """Request a new access token from the API authentication service. - - Returns: - A new access token. - - Raises: - AuthenticationLicenseError: If the user hasn't accepted the license agreement. - RequestsApiError: If the request failed. - """ - try: - response = self.auth_api.login(self.api_token) - return response["id"] - except RequestsApiError as ex: - # Get the original exception that raised. - original_exception = ex.__cause__ - - if isinstance(original_exception, RequestException): - # Get the response from the original request exception. - error_response = ( - # pylint: disable=no-member - original_exception.response - ) - if error_response is not None and error_response.status_code == 401: - try: - error_code = error_response.json()["error"]["name"] - if error_code == "ACCEPT_LICENSE_REQUIRED": - message = error_response.json()["error"]["message"] - raise AuthenticationLicenseError(message) - except (ValueError, KeyError): - # the response did not contain the expected json. - pass - raise - - # User account-related public functions. - - def user_urls(self) -> Dict[str, str]: - """Retrieve the API URLs from the authentication service. - - Returns: - A dict with the base URLs for the services. Currently - supported keys are: - - * ``http``: The API URL for HTTP communication. - * ``ws``: The API URL for websocket communication. - * ``services`: The API URL for additional services. - """ - response = self.auth_api.user_info() - return response["urls"] - - def user_hubs(self) -> List[Dict[str, str]]: - """Retrieve the hub/group/project sets available to the user. - - The first entry in the list will be the default set, as indicated by - the ``isDefault`` field from the API. - - Returns: - A list of dictionaries with the hub, group, and project values keyed by - ``hub``, ``group``, and ``project``, respectively. - """ - response = self.base_api.hubs() - - hubs = [] # type: ignore[var-annotated] - for hub in response: - hub_name = hub["name"] - for group_name, group in hub["groups"].items(): - for project_name, project in group["projects"].items(): - entry = { - "hub": hub_name, - "group": group_name, - "project": project_name, - } - - # Move to the top if it is the default h/g/p. - if project.get("isDefault"): - hubs.insert(0, entry) - else: - hubs.append(entry) - - return hubs - - # Miscellaneous public functions. - - def api_version(self) -> Dict[str, Union[str, bool]]: - """Return the version of the API. - - Returns: - API version. - """ - return self.base_api.version() - - def current_access_token(self) -> Optional[str]: - """Return the current access token. - - Returns: - The access token in use. - """ - return self.access_token - - def current_service_urls(self) -> Dict: - """Return the current service URLs. - - Returns: - A dict with the base URLs for the services, in the same - format as :meth:`user_urls()`. - """ - return self._service_urls diff --git a/qiskit_ibm_runtime/api/clients/backend.py b/qiskit_ibm_runtime/api/clients/backend.py index a043f982f..d779ea7f5 100644 --- a/qiskit_ibm_runtime/api/clients/backend.py +++ b/qiskit_ibm_runtime/api/clients/backend.py @@ -17,7 +17,7 @@ from datetime import datetime as python_datetime from abc import ABC, abstractmethod -from .base import BaseClient +from qiskit_ibm_provider.api.clients.base import BaseClient logger = logging.getLogger(__name__) diff --git a/qiskit_ibm_runtime/api/clients/base.py b/qiskit_ibm_runtime/api/clients/base.py deleted file mode 100644 index 01e65cf7a..000000000 --- a/qiskit_ibm_runtime/api/clients/base.py +++ /dev/null @@ -1,306 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. -# pylint: disable=unused-argument - -"""Base clients for accessing IBM Quantum.""" - -import logging -from typing import Optional, Any, Dict -from queue import Queue -from abc import ABC -from abc import abstractmethod -import traceback -import time -import enum - -from websocket import WebSocketApp, STATUS_NORMAL, STATUS_ABNORMAL_CLOSED - -from ..client_parameters import ClientParameters -from ..exceptions import WebsocketError, WebsocketTimeoutError - -logger = logging.getLogger(__name__) - - -class BaseClient: - """Abstract class for clients.""" - - pass - - -class WebsocketClientCloseCode(enum.IntEnum): - """Possible values used for closing websocket connection.""" - - NORMAL = 1 - TIMEOUT = 2 - PROTOCOL_ERROR = 3 - CANCEL = 4 - - -class BaseWebsocketClient(BaseClient, ABC): - """Base class for websocket clients.""" - - BACKOFF_MAX = 8 - """Maximum time to wait between retries.""" - - def __init__( - self, - websocket_url: str, - client_params: ClientParameters, - job_id: str, - message_queue: Optional[Queue] = None, - ) -> None: - """BaseWebsocketClient constructor. - - Args: - websocket_url: URL for websocket communication with IBM Quantum. - client_params: Parameters used for server connection. - job_id: Job ID. - message_queue: Queue used to hold received messages. - """ - self._websocket_url = websocket_url.rstrip("/") - self._proxy_params = ( - client_params.proxies.to_ws_params(self._websocket_url) if client_params.proxies else {} - ) - self._access_token = client_params.token - self._job_id = job_id - self._message_queue = message_queue - self._header: Optional[Dict] = None - self._ws: Optional[WebSocketApp] = None - - self._authenticated = False - self._cancelled = False - self.connected = False - self._last_message: Any = None - self._current_retry = 0 - self._server_close_code = STATUS_ABNORMAL_CLOSED - self._client_close_code = None - self._error: Optional[str] = None - - def on_open(self, wsa: WebSocketApp) -> None: - """Called when websocket connection established. - - Args: - wsa: WebSocketApp object. - """ - logger.debug("Websocket connection established for job %s", self._job_id) - self.connected = True - if self._cancelled: - # Immediately disconnect if pre-cancelled. - self.disconnect(WebsocketClientCloseCode.CANCEL) - - def on_message(self, wsa: WebSocketApp, message: str) -> None: - """Called when websocket message received. - - Args: - wsa: WebSocketApp object. - message: Message received. - """ - try: - self._handle_message(message) - except Exception as err: # pylint: disable=broad-except - self._error = self._format_exception(err) - self.disconnect(WebsocketClientCloseCode.PROTOCOL_ERROR) - - @abstractmethod - def _handle_message(self, message: str) -> None: - """Handle received message. - - Args: - message: Message received. - """ - pass - - def on_close(self, wsa: WebSocketApp, status_code: int, msg: str) -> None: - """Called when websocket connection clsed. - - Args: - wsa: WebSocketApp object. - status_code: Status code. - msg: Close message. - """ - # Assume abnormal close if no code is given. - self._server_close_code = status_code or STATUS_ABNORMAL_CLOSED - self.connected = False - logger.debug( - "Websocket connection for job %s closed. status code=%s, message=%s", - self._job_id, - status_code, - msg, - ) - - def on_error(self, wsa: WebSocketApp, error: Exception) -> None: - """Called when a websocket error occurred. - - Args: - wsa: WebSocketApp object. - error: Encountered error. - """ - self._error = self._format_exception(error) - - def stream( - self, - url: str, - retries: int = 8, - backoff_factor: float = 0.5, - ) -> Any: - """Stream from the websocket. - - Args: - url: Websocket url to use. - retries: Max number of retries. - backoff_factor: Backoff factor used to calculate the - time to wait between retries. - - Returns: - The final message received. - - Raises: - WebsocketError: If the websocket connection ended unexpectedly. - WebsocketTimeoutError: If the operation timed out. - """ - self._reset_state() - self._cancelled = False - - while self._current_retry <= retries: - self._ws = WebSocketApp( - url, - header=self._header, - on_open=self.on_open, - on_message=self.on_message, - on_error=self.on_error, - on_close=self.on_close, - ) - try: - logger.debug( - "Starting new websocket connection: %s using proxy %s", - url, - self._proxy_params, - ) - self._reset_state() - self._ws.run_forever(ping_interval=60, ping_timeout=10, **self._proxy_params) - self.connected = False - - logger.debug("Websocket run_forever finished.") - - # Handle path-specific errors - self._handle_stream_iteration() - - if self._client_close_code in ( - WebsocketClientCloseCode.NORMAL, - WebsocketClientCloseCode.CANCEL, - ): - # If we closed the connection with a normal code. - return self._last_message - - if self._client_close_code == WebsocketClientCloseCode.TIMEOUT: - raise WebsocketTimeoutError( - "Timeout reached while getting job status." - ) from None - - if self._server_close_code == STATUS_NORMAL and self._error is None: - return self._last_message - - msg_to_log = ( - f"A websocket error occurred while streaming for job " - f"{self._job_id}. Connection closed with {self._server_close_code}." - ) - if self._error is not None: - msg_to_log += f"\n{self._error}" - logger.info(msg_to_log) - - self._current_retry += 1 - if self._current_retry > retries: - error_message = ( - "Max retries exceeded: Failed to establish a websocket connection." - ) - if self._error: - error_message += f" Error: {self._error}" - - raise WebsocketError(error_message) - finally: - self.disconnect(None) - - # Sleep then retry. - backoff_time = self._backoff_time(backoff_factor, self._current_retry) - logger.info( - "Retrying get_job_status via websocket after %s seconds: Attempt #%s", - backoff_time, - self._current_retry, - ) - time.sleep(backoff_time) - - # Execution should not reach here, sanity check. - exception_message = ( - "Max retries exceeded: Failed to establish a websocket " - "connection due to a network error." - ) - - logger.info(exception_message) - raise WebsocketError(exception_message) - - @abstractmethod - def _handle_stream_iteration(self) -> None: - """Called at the end of an iteration.""" - pass - - def _backoff_time(self, backoff_factor: float, current_retry_attempt: int) -> float: - """Calculate the backoff time to wait for. - - Exponential backoff time formula:: - {backoff_factor} * (2 ** (current_retry_attempt - 1)) - - Args: - backoff_factor: Backoff factor, in seconds. - current_retry_attempt: Current number of retry attempts. - - Returns: - The number of seconds to wait for, before making the next retry attempt. - """ - backoff_time = backoff_factor * (2 ** (current_retry_attempt - 1)) - return min(self.BACKOFF_MAX, backoff_time) - - def disconnect( - self, - close_code: Optional[WebsocketClientCloseCode] = WebsocketClientCloseCode.NORMAL, - ) -> None: - """Close the websocket connection. - - Args: - close_code: Disconnect status code. - """ - if self._ws is not None: - logger.debug("Client closing websocket connection with code %s.", close_code) - self._client_close_code = close_code - self._ws.close() - if close_code == WebsocketClientCloseCode.CANCEL: - self._cancelled = True - - def _format_exception(self, error: Exception) -> str: - """Format the exception. - - Args: - error: Exception to be formatted. - - Returns: - Formatted exception. - """ - return "".join( - traceback.format_exception(type(error), error, getattr(error, "__traceback__", "")) - ) - - def _reset_state(self) -> None: - """Reset state for a new connection.""" - self._authenticated = False - self.connected = False - self._error = None - self._server_close_code = None - self._client_close_code = None diff --git a/qiskit_ibm_runtime/api/clients/runtime.py b/qiskit_ibm_runtime/api/clients/runtime.py index f39023e45..5160a8c84 100644 --- a/qiskit_ibm_runtime/api/clients/runtime.py +++ b/qiskit_ibm_runtime/api/clients/runtime.py @@ -322,7 +322,7 @@ def job_logs(self, job_id: str) -> str: """ return self._api.program_job(job_id).logs() - def job_metadata(self, job_id: str) -> str: + def job_metadata(self, job_id: str) -> Dict[str, Any]: """Get job metadata. Args: diff --git a/qiskit_ibm_runtime/api/clients/runtime_ws.py b/qiskit_ibm_runtime/api/clients/runtime_ws.py deleted file mode 100644 index d4710159e..000000000 --- a/qiskit_ibm_runtime/api/clients/runtime_ws.py +++ /dev/null @@ -1,74 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Client for accessing IBM Quantum runtime service.""" - -import logging -from typing import Optional -from queue import Queue - -from .base import BaseWebsocketClient -from ..client_parameters import ClientParameters - -logger = logging.getLogger(__name__) - - -class RuntimeWebsocketClient(BaseWebsocketClient): - """Client for websocket communication with the IBM Quantum runtime service.""" - - def __init__( - self, - websocket_url: str, - client_params: ClientParameters, - job_id: str, - message_queue: Optional[Queue] = None, - ) -> None: - """WebsocketClient constructor. - - Args: - websocket_url: URL for websocket communication with IBM Quantum. - client_params: Parameters used for server connection. - job_id: Job ID. - message_queue: Queue used to hold received messages. - """ - super().__init__(websocket_url, client_params, job_id, message_queue) - self._header = client_params.get_auth_handler().get_headers() - - def _handle_message(self, message: str) -> None: - """Handle received message. - - Args: - message: Message received. - """ - if not self._authenticated: - self._authenticated = True # First message is an ACK - else: - self._message_queue.put_nowait(message) - self._current_retry = 0 - - def job_results(self, max_retries: int = 8, backoff_factor: float = 0.5) -> None: - """Return the interim result of a runtime job. - - Args: - max_retries: Max number of retries. - backoff_factor: Backoff factor used to calculate the - time to wait between retries. - - Raises: - WebsocketError: If a websocket error occurred. - """ - url = "{}/stream/jobs/{}".format(self._websocket_url, self._job_id) - self.stream(url=url, retries=max_retries, backoff_factor=backoff_factor) - - def _handle_stream_iteration(self) -> None: - """Handle a streaming iteration.""" - pass diff --git a/qiskit_ibm_runtime/api/clients/version.py b/qiskit_ibm_runtime/api/clients/version.py deleted file mode 100644 index 68b2c13c2..000000000 --- a/qiskit_ibm_runtime/api/clients/version.py +++ /dev/null @@ -1,48 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Client for determining the version of an IBM Quantum service.""" - -from typing import Dict, Union, Any - -from ..session import RetrySession -from ..rest.root import Api - -from .base import BaseClient - - -class VersionClient(BaseClient): - """Client for determining the version of an IBM Quantum service.""" - - def __init__(self, url: str, **request_kwargs: Any) -> None: - """VersionClient constructor. - - Args: - url: URL of the service. - **request_kwargs: Arguments for the request ``Session``. - """ - self.client_version_finder = Api(RetrySession(url, **request_kwargs)) - - def version(self) -> Dict[str, Union[bool, str]]: - """Return the version information. - - Returns: - A dictionary with information about the API version, - with the following keys: - - * ``new_api`` (bool): Whether the new API is being used - - And the following optional keys: - - * ``api-*`` (str): The versions of each individual API component - """ - return self.client_version_finder.version() diff --git a/qiskit_ibm_runtime/api/rest/__init__.py b/qiskit_ibm_runtime/api/rest/__init__.py index f98193de6..32092ddd9 100644 --- a/qiskit_ibm_runtime/api/rest/__init__.py +++ b/qiskit_ibm_runtime/api/rest/__init__.py @@ -16,4 +16,4 @@ Job adaptor, for example, handles all /Jobs/{job id} endpoints. """ -from .root import Api +from qiskit_ibm_provider.api.rest.root import Api diff --git a/qiskit_ibm_runtime/api/rest/backend.py b/qiskit_ibm_runtime/api/rest/backend.py deleted file mode 100644 index 777667b6d..000000000 --- a/qiskit_ibm_runtime/api/rest/backend.py +++ /dev/null @@ -1,137 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Backend REST adapter.""" - -import json -from typing import Dict, Optional, Any, List -from datetime import datetime - -from .base import RestAdapterBase -from ..session import RetrySession -from .utils.data_mapper import map_jobs_limit_response - - -class Backend(RestAdapterBase): - """Rest adapter for backend related endpoints.""" - - URL_MAP = { - "properties": "/properties", - "pulse_defaults": "/defaults", - "status": "/queue/status", - "jobs_limit": "/jobsLimit", - "bookings": "/bookings/v2", - } - - def __init__(self, session: RetrySession, backend_name: str, url_prefix: str = "") -> None: - """Backend constructor. - - Args: - session: Session to be used in the adaptor. - backend_name: Name of the backend. - url_prefix: Base URL. - """ - self.backend_name = backend_name - super().__init__(session, "{}/devices/{}".format(url_prefix, backend_name)) - - def properties(self, datetime: Optional[datetime] = None) -> Dict[str, Any]: - """Return backend properties. - - Args: - datetime: Date and time used for additional filtering passed to the query. - - Returns: - JSON response of backend properties. - """ - # pylint: disable=redefined-outer-name - url = self.get_url("properties") - - params = {"version": 1} - - query = {} - if datetime: - extra_filter = {"last_update_date": {"lt": datetime.isoformat()}} - query["where"] = extra_filter - params["filter"] = json.dumps(query) # type: ignore[assignment] - - response = self.session.get(url, params=params).json() - - # Adjust name of the backend. - if response: - response["backend_name"] = self.backend_name - - return response - - def pulse_defaults(self) -> Dict[str, Any]: - """Return backend pulse defaults. - - Returns: - JSON response of pulse defaults. - """ - url = self.get_url("pulse_defaults") - return self.session.get(url).json() - - def status(self) -> Dict[str, Any]: - """Return backend status. - - Returns: - JSON response of backend status. - """ - url = self.get_url("status") - response = self.session.get(url).json() - - # Adjust fields according to the specs (BackendStatus). - ret = { - "backend_name": self.backend_name, - "backend_version": response.get("backend_version", "0.0.0"), - "status_msg": response.get("status", ""), - "operational": bool(response.get("state", False)), - } - - # 'pending_jobs' is required, and should be >= 0. - if "lengthQueue" in response: - ret["pending_jobs"] = max(response["lengthQueue"], 0) - else: - ret["pending_jobs"] = 0 - - return ret - - def job_limit(self) -> Dict[str, Any]: - """Return backend job limit. - - Returns: - JSON response of job limit. - """ - url = self.get_url("jobs_limit") - return map_jobs_limit_response(self.session.get(url).json()) - - def reservations( - self, - start_datetime: Optional[datetime] = None, - end_datetime: Optional[datetime] = None, - ) -> List: - """Return backend reservation information. - - Args: - start_datetime: Starting datetime in UTC. - end_datetime: Ending datetime in UTC. - - Returns: - JSON response. - """ - params = {} - if start_datetime: - params["initialDate"] = start_datetime.isoformat() - if end_datetime: - params["endDate"] = end_datetime.isoformat() - url = self.get_url("bookings") - return self.session.get(url, params=params).json() diff --git a/qiskit_ibm_runtime/api/rest/base.py b/qiskit_ibm_runtime/api/rest/base.py deleted file mode 100644 index 809dc606f..000000000 --- a/qiskit_ibm_runtime/api/rest/base.py +++ /dev/null @@ -1,45 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Base REST adapter.""" - -from ..session import RetrySession - - -class RestAdapterBase: - """Base class for REST adapters.""" - - URL_MAP = {} # type: ignore[var-annotated] - """Mapping between the internal name of an endpoint and the actual URL.""" - - _HEADER_JSON_CONTENT = {"Content-Type": "application/json"} - - def __init__(self, session: RetrySession, prefix_url: str = "") -> None: - """RestAdapterBase constructor. - - Args: - session: Session to be used in the adapter. - prefix_url: String to be prepend to all URLs. - """ - self.session = session - self.prefix_url = prefix_url - - def get_url(self, identifier: str) -> str: - """Return the resolved URL for the specified identifier. - - Args: - identifier: Internal identifier of the endpoint. - - Returns: - The resolved URL of the endpoint (relative to the session base URL). - """ - return "{}{}".format(self.prefix_url, self.URL_MAP[identifier]) diff --git a/qiskit_ibm_runtime/api/rest/cloud_backend.py b/qiskit_ibm_runtime/api/rest/cloud_backend.py index 8311730b0..237ceef06 100644 --- a/qiskit_ibm_runtime/api/rest/cloud_backend.py +++ b/qiskit_ibm_runtime/api/rest/cloud_backend.py @@ -14,7 +14,7 @@ from typing import Dict, Any -from .base import RestAdapterBase +from qiskit_ibm_provider.api.rest.base import RestAdapterBase from ..session import RetrySession diff --git a/qiskit_ibm_runtime/api/rest/program.py b/qiskit_ibm_runtime/api/rest/program.py index 0e2f6c94e..ce76bdeac 100644 --- a/qiskit_ibm_runtime/api/rest/program.py +++ b/qiskit_ibm_runtime/api/rest/program.py @@ -15,7 +15,7 @@ from typing import Dict, Any, Optional from concurrent import futures -from .base import RestAdapterBase +from qiskit_ibm_provider.api.rest.base import RestAdapterBase from ..session import RetrySession diff --git a/qiskit_ibm_runtime/api/rest/program_job.py b/qiskit_ibm_runtime/api/rest/program_job.py deleted file mode 100644 index c74fca8cb..000000000 --- a/qiskit_ibm_runtime/api/rest/program_job.py +++ /dev/null @@ -1,97 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Program Job REST adapter.""" - -from typing import Dict - -from .base import RestAdapterBase -from ..session import RetrySession - - -class ProgramJob(RestAdapterBase): - """Rest adapter for program job related endpoints.""" - - URL_MAP = { - "self": "", - "results": "/results", - "cancel": "/cancel", - "logs": "/logs", - "interim_results": "/interim_results", - "metrics": "/metrics", - } - - def __init__(self, session: RetrySession, job_id: str, url_prefix: str = "") -> None: - """ProgramJob constructor. - - Args: - session: Session to be used in the adapter. - job_id: ID of the program job. - url_prefix: Prefix to use in the URL. - """ - super().__init__(session, "{}/jobs/{}".format(url_prefix, job_id)) - - def get(self, exclude_params: bool = None) -> Dict: - """Return program job information. - - Args: - exclude_params: If ``True``, the params will not be included in the response. - - Returns: - JSON response. - """ - payload = {} - if exclude_params: - payload["exclude_params"] = "true" - return self.session.get(self.get_url("self"), params=payload).json() - - def delete(self) -> None: - """Delete program job.""" - self.session.delete(self.get_url("self")) - - def interim_results(self) -> str: - """Return program job interim results. - - Returns: - Interim results. - """ - response = self.session.get(self.get_url("interim_results")) - return response.text - - def results(self) -> str: - """Return program job results. - - Returns: - Job results. - """ - response = self.session.get(self.get_url("results")) - return response.text - - def cancel(self) -> None: - """Cancel the job.""" - self.session.post(self.get_url("cancel")) - - def logs(self) -> str: - """Retrieve job logs. - - Returns: - Job logs. - """ - return self.session.get(self.get_url("logs")).text - - def metadata(self) -> str: - """Retrieve job metadata. - - Returns: - Job Metadata. - """ - return self.session.get(self.get_url("metrics")).text diff --git a/qiskit_ibm_runtime/api/rest/root.py b/qiskit_ibm_runtime/api/rest/root.py deleted file mode 100644 index f3b35277c..000000000 --- a/qiskit_ibm_runtime/api/rest/root.py +++ /dev/null @@ -1,100 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Root REST adapter.""" - -import logging -from typing import Dict, List, Any, Union -import json - -from .base import RestAdapterBase - -logger = logging.getLogger(__name__) - - -class Api(RestAdapterBase): - """Rest adapter for general endpoints.""" - - URL_MAP = { - "login": "/users/loginWithToken", - "user_info": "/users/me", - "hubs": "/Network", - "version": "/version", - "bookings": "/Network/bookings/v2", - } - - # Client functions. - - def hubs(self) -> List[Dict[str, Any]]: - """Return the list of hub/group/project sets available to the user. - - Returns: - JSON response. - """ - url = self.get_url("hubs") - return self.session.get(url).json() - - def version(self) -> Dict[str, Union[str, bool]]: - """Return the version information. - - Returns: - A dictionary with information about the API version, - with the following keys: - - * ``new_api`` (bool): Whether the new API is being used - - And the following optional keys: - - * ``api-*`` (str): The versions of each individual API component - """ - url = self.get_url("version") - response = self.session.get(url) - - try: - version_info = response.json() - version_info["new_api"] = True - except json.JSONDecodeError: - return {"new_api": False, "api": response.text} - - return version_info - - def login(self, api_token: str) -> Dict[str, Any]: - """Login with token. - - Args: - api_token: API token. - - Returns: - JSON response. - """ - url = self.get_url("login") - return self.session.post(url, json={"apiToken": api_token}).json() - - def user_info(self) -> Dict[str, Any]: - """Return user information. - - Returns: - JSON response of user information. - """ - url = self.get_url("user_info") - response = self.session.get(url).json() - - return response - - def reservations(self) -> List: - """Return reservation information. - - Returns: - JSON response. - """ - url = self.get_url("bookings") - return self.session.get(url).json() diff --git a/qiskit_ibm_runtime/api/rest/runtime.py b/qiskit_ibm_runtime/api/rest/runtime.py index b34a2b293..f11e752e6 100644 --- a/qiskit_ibm_runtime/api/rest/runtime.py +++ b/qiskit_ibm_runtime/api/rest/runtime.py @@ -17,9 +17,9 @@ from typing import Dict, Any, List, Union, Optional import json -from .base import RestAdapterBase +from qiskit_ibm_provider.api.rest.base import RestAdapterBase +from qiskit_ibm_provider.api.rest.program_job import ProgramJob from .program import Program -from .program_job import ProgramJob from .runtime_session import RuntimeSession from ...utils import RuntimeEncoder from ...utils.converters import local_to_utc diff --git a/qiskit_ibm_runtime/api/rest/runtime_session.py b/qiskit_ibm_runtime/api/rest/runtime_session.py index b8f0c5a92..5faceb145 100644 --- a/qiskit_ibm_runtime/api/rest/runtime_session.py +++ b/qiskit_ibm_runtime/api/rest/runtime_session.py @@ -13,7 +13,7 @@ """Runtime Session REST adapter.""" -from .base import RestAdapterBase +from qiskit_ibm_provider.api.rest.base import RestAdapterBase from ..session import RetrySession diff --git a/qiskit_ibm_runtime/runtime_job.py b/qiskit_ibm_runtime/runtime_job.py index 76ab502e4..0f052aed9 100644 --- a/qiskit_ibm_runtime/runtime_job.py +++ b/qiskit_ibm_runtime/runtime_job.py @@ -390,8 +390,7 @@ def metrics(self) -> Dict[str, Any]: version="0.11.1", remedy="Use the 'usage.seconds' attribute instead.", ) - metadata_str = self._api_client.job_metadata(self.job_id()) - return json.loads(metadata_str) + return self._api_client.job_metadata(self.job_id()) except RequestsApiError as err: raise IBMRuntimeError(f"Failed to get job metadata: {err}") from None @@ -574,10 +573,7 @@ def inputs(self) -> Dict: """ if not self._params: response = self._api_client.job_get(job_id=self.job_id()) - params = response.get("params", {}) - if not isinstance(params, str): - params = json.dumps(params) - self._params = json.loads(str(params), cls=RuntimeDecoder) + self._params = response.get("params", {}) return self._params @property diff --git a/requirements.txt b/requirements.txt index 350aa469f..a72802519 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,4 @@ python-dateutil>=2.8.0 websocket-client>=1.5.1 typing-extensions>=4.0.0 ibm-platform-services>=0.22.6 -qiskit-ibm-provider>=0.5.3 +qiskit-ibm-provider>=0.6.0 \ No newline at end of file diff --git a/setup.py b/setup.py index b1748843c..9958d9c94 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ "python-dateutil>=2.8.0", "websocket-client>=1.5.1", "ibm-platform-services>=0.22.6", - "qiskit-ibm-provider>=0.5.3", + "qiskit-ibm-provider>=0.6.0", ] # Handle version. diff --git a/test/integration/test_auth_client.py b/test/integration/test_auth_client.py index b05a8f7fb..37c4d19bb 100644 --- a/test/integration/test_auth_client.py +++ b/test/integration/test_auth_client.py @@ -14,10 +14,9 @@ import re +from qiskit_ibm_provider.api.exceptions import RequestsApiError from qiskit_ibm_runtime.api.client_parameters import ClientParameters from qiskit_ibm_runtime.api.clients import AuthClient -from qiskit_ibm_runtime.api.exceptions import ApiError -from qiskit_ibm_runtime.exceptions import IBMNotAuthorizedError from ..ibm_test_case import IBMTestCase from ..decorators import integration_test_setup, IntegrationTestDependencies @@ -35,21 +34,21 @@ def test_valid_login(self, dependencies: IntegrationTestDependencies) -> None: def test_url_404(self, dependencies: IntegrationTestDependencies) -> None: """Test login against a 404 URL""" url_404 = re.sub(r"/api.*$", "/api/TEST_404", dependencies.url) - with self.assertRaises(ApiError): + with self.assertRaises(RequestsApiError): _ = self._init_auth_client(dependencies.token, url_404) @integration_test_setup(supported_channel=["ibm_quantum"], init_service=False) def test_invalid_token(self, dependencies: IntegrationTestDependencies) -> None: """Test login using invalid token.""" qe_token = "INVALID_TOKEN" - with self.assertRaises(IBMNotAuthorizedError): + with self.assertRaises(RequestsApiError): _ = self._init_auth_client(qe_token, dependencies.url) @integration_test_setup(supported_channel=["ibm_quantum"], init_service=False) def test_url_unreachable(self, dependencies: IntegrationTestDependencies) -> None: """Test login against an invalid (malformed) URL.""" qe_url = "INVALID_URL" - with self.assertRaises(ApiError): + with self.assertRaises(RequestsApiError): _ = self._init_auth_client(dependencies.token, qe_url) @integration_test_setup(supported_channel=["ibm_quantum"], init_service=False) diff --git a/test/integration/test_proxies.py b/test/integration/test_proxies.py index 1b129cfcb..6f63e26f7 100644 --- a/test/integration/test_proxies.py +++ b/test/integration/test_proxies.py @@ -18,11 +18,12 @@ from requests.exceptions import ProxyError from qiskit_ibm_provider.proxies import ProxyConfiguration +from qiskit_ibm_provider.api.exceptions import RequestsApiError as ProviderRequestsApiError from qiskit_ibm_runtime import QiskitRuntimeService from qiskit_ibm_runtime.api.client_parameters import ClientParameters from qiskit_ibm_runtime.api.clients import AuthClient, VersionClient from qiskit_ibm_runtime.api.clients.runtime import RuntimeClient -from qiskit_ibm_runtime.api.exceptions import RequestsApiError +from qiskit_ibm_runtime.api.exceptions import RequestsApiError as RuntimeRequestsApiError from ..ibm_test_case import IBMTestCase from ..decorators import IntegrationTestDependencies, integration_test_setup @@ -159,7 +160,7 @@ def test_invalid_proxy_port_runtime_client( url=dependencies.url, proxies=ProxyConfiguration(urls=INVALID_PORT_PROXIES), ) - with self.assertRaises(RequestsApiError) as context_manager: + with self.assertRaises(RuntimeRequestsApiError) as context_manager: client = RuntimeClient(params) client.list_programs(limit=1) self.assertIsInstance(context_manager.exception.__cause__, ProxyError) @@ -173,7 +174,7 @@ def test_invalid_proxy_port_authclient(self, dependencies: IntegrationTestDepend url=dependencies.url, proxies=ProxyConfiguration(urls=INVALID_PORT_PROXIES), ) - with self.assertRaises(RequestsApiError) as context_manager: + with self.assertRaises(ProviderRequestsApiError) as context_manager: _ = AuthClient(params) self.assertIsInstance(context_manager.exception.__cause__, ProxyError) @@ -183,7 +184,7 @@ def test_invalid_proxy_port_versionclient( self, dependencies: IntegrationTestDependencies ) -> None: """Should raise RequestApiError with ProxyError using VersionClient.""" - with self.assertRaises(RequestsApiError) as context_manager: + with self.assertRaises(ProviderRequestsApiError) as context_manager: version_finder = VersionClient(dependencies.url, proxies=INVALID_PORT_PROXIES) version_finder.version() @@ -200,7 +201,7 @@ def test_invalid_proxy_address_runtime_client( url=dependencies.url, proxies=ProxyConfiguration(urls=INVALID_ADDRESS_PROXIES), ) - with self.assertRaises(RequestsApiError) as context_manager: + with self.assertRaises(RuntimeRequestsApiError) as context_manager: client = RuntimeClient(params) client.list_programs(limit=1) @@ -217,7 +218,7 @@ def test_invalid_proxy_address_authclient( url=dependencies.url, proxies=ProxyConfiguration(urls=INVALID_ADDRESS_PROXIES), ) - with self.assertRaises(RequestsApiError) as context_manager: + with self.assertRaises(ProviderRequestsApiError) as context_manager: _ = AuthClient(params) self.assertIsInstance(context_manager.exception.__cause__, ProxyError) @@ -227,7 +228,7 @@ def test_invalid_proxy_address_versionclient( self, dependencies: IntegrationTestDependencies ) -> None: """Should raise RequestApiError with ProxyError using VersionClient.""" - with self.assertRaises(RequestsApiError) as context_manager: + with self.assertRaises(ProviderRequestsApiError) as context_manager: version_finder = VersionClient(dependencies.url, proxies=INVALID_ADDRESS_PROXIES) version_finder.version()