diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f18b8cf0f..e006971b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,7 +111,7 @@ jobs: matrix: python-version: [ 3.9 ] os: [ "ubuntu-latest" ] - environment: [ "legacy-production" ] + environment: [ "ibm-quantum-production" ] environment: ${{ matrix.environment }} env: QISKIT_IBM_TOKEN: ${{ secrets.QISKIT_IBM_TOKEN }} diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 5e02f3931..9f27b4ba0 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -26,7 +26,7 @@ jobs: matrix: python-version: [ 3.9 ] os: [ "ubuntu-latest" ] - environment: [ "legacy-production", "legacy-staging" ] + environment: [ "ibm-quantum-production", "ibm-quantum-staging" ] environment: ${{ matrix.environment }} env: QISKIT_IBM_TOKEN: ${{ secrets.QISKIT_IBM_TOKEN }} diff --git a/qiskit_ibm_provider/accounts/__init__.py b/qiskit_ibm_provider/accounts/__init__.py index 671e872ab..fdfbbf483 100644 --- a/qiskit_ibm_provider/accounts/__init__.py +++ b/qiskit_ibm_provider/accounts/__init__.py @@ -14,7 +14,7 @@ Account management functionality. """ -from .account import Account, AccountType +from .account import Account, AccountType, ChannelType from .management import AccountManager from .exceptions import ( AccountNotFoundError, diff --git a/qiskit_ibm_provider/accounts/account.py b/qiskit_ibm_provider/accounts/account.py index 992f909e0..55d721c3a 100644 --- a/qiskit_ibm_provider/accounts/account.py +++ b/qiskit_ibm_provider/accounts/account.py @@ -20,13 +20,14 @@ from typing_extensions import Literal from .exceptions import InvalidAccountError -from ..api.auth import LegacyAuth +from ..api.auth import QuantumAuth from ..proxies import ProxyConfiguration from ..utils.hgp import from_instance_format AccountType = Optional[Literal["cloud", "legacy"]] +ChannelType = Optional[Literal["ibm_cloud", "ibm_quantum"]] -LEGACY_API_URL = "https://auth.quantum-computing.ibm.com/api" +IBM_QUANTUM_API_URL = "https://auth.quantum-computing.ibm.com/api" logger = logging.getLogger(__name__) @@ -35,7 +36,7 @@ class Account: def __init__( self, - auth: AccountType, + channel: ChannelType, token: str, url: Optional[str] = None, instance: Optional[str] = None, @@ -45,16 +46,16 @@ def __init__( """Account constructor. Args: - auth: Authentication type, ``cloud`` or ``legacy``. + channel: Channel type, ``ibm_cloud`` or ``ibm_quantum``. token: Account token to use. url: Authentication URL. instance: Service instance to use. proxies: Proxy configuration. verify: Whether to verify server's TLS certificate. """ - resolved_url = url or LEGACY_API_URL + resolved_url = url or IBM_QUANTUM_API_URL - self.auth = auth + self.channel = channel self.token = token self.url = resolved_url self.instance = instance @@ -73,7 +74,7 @@ def from_saved_format(cls, data: dict) -> "Account": """Creates an account instance from data saved on disk.""" proxies = data.get("proxies") return cls( - auth=data.get("auth"), + channel=data.get("channel"), url=data.get("url"), token=data.get("token"), instance=data.get("instance"), @@ -83,14 +84,14 @@ def from_saved_format(cls, data: dict) -> "Account": def get_auth_handler(self) -> AuthBase: """Returns the respective authentication handler.""" - return LegacyAuth(access_token=self.token) + return QuantumAuth(access_token=self.token) def __eq__(self, other: object) -> bool: if not isinstance(other, Account): return False return all( [ - self.auth == other.auth, + self.channel == other.channel, self.token == other.token, self.url == other.url, self.instance == other.instance, @@ -109,19 +110,20 @@ def validate(self) -> "Account": This Account instance. """ - self._assert_valid_auth(self.auth) + self._assert_valid_channel(self.channel) self._assert_valid_token(self.token) self._assert_valid_url(self.url) - self._assert_valid_instance(self.auth, self.instance) + self._assert_valid_instance(self.channel, self.instance) self._assert_valid_proxies(self.proxies) return self @staticmethod - def _assert_valid_auth(auth: AccountType) -> None: - """Assert that the auth parameter is valid.""" - if not (auth in ["cloud", "legacy"]): + def _assert_valid_channel(channel: ChannelType) -> None: + """Assert that the channel parameter is valid.""" + if not (channel in ["ibm_cloud", "ibm_quantum"]): raise InvalidAccountError( - f"Invalid `auth` value. Expected one of ['cloud', 'legacy'], got '{auth}'." + f"Invalid `channel` value. Expected one of " + f"{['ibm_cloud', 'ibm_quantum']}, got '{channel}'." ) @staticmethod @@ -149,14 +151,14 @@ def _assert_valid_proxies(config: ProxyConfiguration) -> None: config.validate() @staticmethod - def _assert_valid_instance(auth: AccountType, instance: str) -> None: + def _assert_valid_instance(channel: ChannelType, instance: str) -> None: """Assert that the instance name is valid for the given account type.""" - if auth == "cloud": + if channel == "ibm_cloud": if not (isinstance(instance, str) and len(instance) > 0): raise InvalidAccountError( f"Invalid `instance` value. Expected a non-empty string, got '{instance}'." ) - if auth == "legacy": + if channel == "ibm_quantum": if instance is not None: try: from_instance_format(instance) diff --git a/qiskit_ibm_provider/accounts/management.py b/qiskit_ibm_provider/accounts/management.py index e0e750a73..3ba4df85e 100644 --- a/qiskit_ibm_provider/accounts/management.py +++ b/qiskit_ibm_provider/accounts/management.py @@ -15,7 +15,7 @@ import os from typing import Optional, Dict from .exceptions import AccountNotFoundError -from .account import Account, AccountType +from .account import Account, ChannelType from ..proxies import ProxyConfiguration from .storage import save_config, read_config, delete_config @@ -25,8 +25,10 @@ _DEFAULT_ACCOUNT_NAME = "default" _DEFAULT_ACCOUNT_NAME_LEGACY = "default-legacy" _DEFAULT_ACCOUNT_NAME_CLOUD = "default-cloud" -_DEFAULT_ACCOUNT_TYPE: AccountType = "legacy" -_ACCOUNT_TYPES = [_DEFAULT_ACCOUNT_TYPE, "legacy"] +_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM = "default-ibm-quantum" +_DEFAULT_ACCOUNT_NAME_IBM_CLOUD = "default-ibm-cloud" +_DEFAULT_CHANNEL_TYPE: ChannelType = "ibm_cloud" +_CHANNEL_TYPES = [_DEFAULT_CHANNEL_TYPE, "ibm_quantum"] class AccountManager: @@ -38,23 +40,24 @@ def save( token: Optional[str] = None, url: Optional[str] = None, instance: Optional[str] = None, - auth: Optional[AccountType] = None, + channel: Optional[ChannelType] = None, name: Optional[str] = _DEFAULT_ACCOUNT_NAME, proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = None, overwrite: Optional[bool] = False, ) -> None: """Save account on disk.""" - config_key = name or cls._get_default_account_name(auth) + cls.migrate() + name = name or cls._get_default_account_name(channel) return save_config( filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE, - name=config_key, + name=name, overwrite=overwrite, config=Account( token=token, url=url, instance=instance, - auth=auth, + channel=channel, proxies=proxies, verify=verify, ) @@ -65,22 +68,23 @@ def save( @staticmethod def list( default: Optional[bool] = None, - auth: Optional[str] = None, + channel: Optional[ChannelType] = None, name: Optional[str] = None, ) -> Dict[str, Account]: """List all accounts saved on disk.""" + AccountManager.migrate() def _matching_name(account_name: str) -> bool: return name is None or name == account_name - def _matching_auth(account: Account) -> bool: - return auth is None or account.auth == auth + def _matching_channel(account: Account) -> bool: + return channel is None or account.channel == channel def _matching_default(account_name: str) -> bool: default_accounts = [ _DEFAULT_ACCOUNT_NAME, - _DEFAULT_ACCOUNT_NAME_LEGACY, - _DEFAULT_ACCOUNT_NAME_CLOUD, + _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM, + _DEFAULT_ACCOUNT_NAME_IBM_CLOUD, ] if default is None: return True @@ -91,7 +95,10 @@ def _matching_default(account_name: str) -> bool: # load all accounts all_accounts = map( - lambda kv: (kv[0], Account.from_saved_format(kv[1])), + lambda kv: ( + kv[0], + Account.from_saved_format(kv[1]), + ), read_config(filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE).items(), ) @@ -99,7 +106,7 @@ def _matching_default(account_name: str) -> bool: filtered_accounts = dict( list( filter( - lambda kv: _matching_auth(kv[1]) + lambda kv: _matching_channel(kv[1]) and _matching_default(kv[0]) and _matching_name(kv[0]), all_accounts, @@ -111,13 +118,13 @@ def _matching_default(account_name: str) -> bool: @classmethod def get( - cls, name: Optional[str] = None, auth: Optional[AccountType] = None + cls, name: Optional[str] = None, channel: Optional[ChannelType] = None ) -> Optional[Account]: """Read account from disk. Args: name: Account name. Takes precedence if `auth` is also specified. - auth: Account auth type. + channel: Channel type. Returns: Account information. @@ -125,6 +132,7 @@ def get( Raises: AccountNotFoundError: If the input value cannot be found on disk. """ + cls.migrate() if name: saved_account = read_config( filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE, name=name @@ -135,23 +143,23 @@ def get( ) return Account.from_saved_format(saved_account) - auth_ = auth or _DEFAULT_ACCOUNT_TYPE - env_account = cls._from_env_variables(auth_) + channel_ = channel or _DEFAULT_CHANNEL_TYPE + env_account = cls._from_env_variables(channel_) if env_account is not None: return env_account - if auth: + if channel: saved_account = read_config( filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE, - name=cls._get_default_account_name(auth), + name=cls._get_default_account_name(channel=channel), ) if saved_account is None: - raise AccountNotFoundError(f"No default {auth} account saved.") + raise AccountNotFoundError(f"No default {channel} account saved.") return Account.from_saved_format(saved_account) all_config = read_config(filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE) - for account_type in _ACCOUNT_TYPES: - account_name = cls._get_default_account_name(account_type) + for channel_type in _CHANNEL_TYPES: + account_name = cls._get_default_account_name(channel_type) if account_name in all_config: return Account.from_saved_format(all_config[account_name]) @@ -161,30 +169,70 @@ def get( def delete( cls, name: Optional[str] = None, - auth: Optional[str] = None, + channel: Optional[ChannelType] = None, ) -> bool: """Delete account from disk.""" + cls.migrate() + name = name or cls._get_default_account_name(channel) + return delete_config(name=name, filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE) - config_key = name or cls._get_default_account_name(auth) - return delete_config( - name=config_key, filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE - ) + @classmethod + def migrate(cls) -> None: + """Migrate accounts on disk by removing `auth` and adding `channel`.""" + data = read_config(filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE) + for key, value in data.items(): + if key == _DEFAULT_ACCOUNT_NAME_CLOUD: + value.pop("auth", None) + value.update(channel="ibm_cloud") + delete_config(filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE, name=key) + save_config( + filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE, + name=_DEFAULT_ACCOUNT_NAME_IBM_CLOUD, + config=value, + overwrite=False, + ) + elif key == _DEFAULT_ACCOUNT_NAME_LEGACY: + value.pop("auth", None) + value.update(channel="ibm_quantum") + delete_config(filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE, name=key) + save_config( + filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE, + name=_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM, + config=value, + overwrite=False, + ) + else: + if hasattr(value, "auth"): + if value["auth"] == "cloud": + value.update(channel="ibm_cloud") + elif value["auth"] == "legacy": + value.update(channel="ibm_quantum") + value.pop("auth", None) + save_config( + filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE, + name=key, + config=value, + overwrite=True, + ) @classmethod - def _from_env_variables(cls, auth: Optional[AccountType]) -> Optional[Account]: + def _from_env_variables(cls, channel: Optional[ChannelType]) -> Optional[Account]: """Read account from environment variable.""" token = os.getenv("QISKIT_IBM_TOKEN") url = os.getenv("QISKIT_IBM_URL") if not (token and url): return None return Account( - token=token, url=url, instance=os.getenv("QISKIT_IBM_INSTANCE"), auth=auth + token=token, + url=url, + instance=os.getenv("QISKIT_IBM_INSTANCE"), + channel=channel, ) @classmethod - def _get_default_account_name(cls, auth: AccountType) -> str: + def _get_default_account_name(cls, channel: ChannelType) -> str: return ( - _DEFAULT_ACCOUNT_NAME_LEGACY - if auth == "legacy" - else _DEFAULT_ACCOUNT_NAME_CLOUD + _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM + if channel == "ibm_quantum" + else _DEFAULT_ACCOUNT_NAME_IBM_CLOUD ) diff --git a/qiskit_ibm_provider/accounts/storage.py b/qiskit_ibm_provider/accounts/storage.py index a3948b61f..7e6fcdcee 100644 --- a/qiskit_ibm_provider/accounts/storage.py +++ b/qiskit_ibm_provider/accounts/storage.py @@ -54,7 +54,6 @@ def read_config( return data if name in data: return data[name] - return None diff --git a/qiskit_ibm_provider/api/auth.py b/qiskit_ibm_provider/api/auth.py index b1a5ba3b2..e9a2c79cf 100644 --- a/qiskit_ibm_provider/api/auth.py +++ b/qiskit_ibm_provider/api/auth.py @@ -18,14 +18,14 @@ from requests.auth import AuthBase -class LegacyAuth(AuthBase): +class QuantumAuth(AuthBase): """Attaches Legacy Authentication to the given Request object.""" def __init__(self, access_token: str): self.access_token = access_token def __eq__(self, other: object) -> bool: - if isinstance(other, LegacyAuth): + if isinstance(other, QuantumAuth): return self.access_token == other.access_token return False diff --git a/qiskit_ibm_provider/api/client_parameters.py b/qiskit_ibm_provider/api/client_parameters.py index 2c8ee24f4..27c851455 100644 --- a/qiskit_ibm_provider/api/client_parameters.py +++ b/qiskit_ibm_provider/api/client_parameters.py @@ -14,7 +14,7 @@ from typing import Dict, Optional, Any -from ..api.auth import LegacyAuth +from ..api.auth import QuantumAuth from ..proxies import ProxyConfiguration TEMPLATE_IBM_HUBS = "{prefix}/Network/{hub}/Groups/{group}/Projects/{project}" @@ -47,9 +47,9 @@ def __init__( self.proxies = proxies self.verify = verify - def get_auth_handler(self) -> LegacyAuth: + def get_auth_handler(self) -> QuantumAuth: """Returns the respective authentication handler.""" - return LegacyAuth(access_token=self.token) + return QuantumAuth(access_token=self.token) def connection_parameters(self) -> Dict[str, Any]: """Construct connection related parameters. diff --git a/qiskit_ibm_provider/api/clients/auth.py b/qiskit_ibm_provider/api/clients/auth.py index fd679b827..fabe0840c 100644 --- a/qiskit_ibm_provider/api/clients/auth.py +++ b/qiskit_ibm_provider/api/clients/auth.py @@ -15,7 +15,7 @@ from typing import Dict, List, Optional, Any, Union from requests.exceptions import RequestException -from ..auth import LegacyAuth +from ..auth import QuantumAuth from ..exceptions import AuthenticationLicenseError, RequestsApiError from ..rest import Api from ..session import RetrySession @@ -55,14 +55,14 @@ def _init_service_clients(self, **request_kwargs: Any) -> Api: """ # Request an access token. self.access_token = self._request_access_token() - self.auth_api.session.auth = LegacyAuth(access_token=self.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=LegacyAuth(access_token=self.access_token), + auth=QuantumAuth(access_token=self.access_token), **request_kwargs, ) ) diff --git a/qiskit_ibm_provider/ibm_provider.py b/qiskit_ibm_provider/ibm_provider.py index fce9e73b3..450706693 100644 --- a/qiskit_ibm_provider/ibm_provider.py +++ b/qiskit_ibm_provider/ibm_provider.py @@ -170,7 +170,7 @@ def __init__( proxies=self._account.proxies, verify=self._account.verify, ) - self._auth_client = self._authenticate_legacy_account(self._client_params) + self._auth_client = self._authenticate_ibm_quantum_account(self._client_params) self._hgps = self._initialize_hgps(self._auth_client) self._initialize_services() @@ -196,7 +196,7 @@ def _discover_account( else: if token: account = Account( - auth="legacy", + channel="ibm_quantum", token=token, url=url, instance=instance, @@ -206,9 +206,9 @@ def _discover_account( else: if url: logger.warning( - "Loading default legacy account. Input 'url' is ignored.", + "Loading default ibm_quantum account. Input 'url' is ignored." ) - account = AccountManager.get(auth="legacy") + account = AccountManager.get(channel="ibm_quantum") if account is None: account = AccountManager.get() @@ -294,7 +294,7 @@ def _initialize_hgps( ) return hgps - def _authenticate_legacy_account( + def _authenticate_ibm_quantum_account( self, client_params: ClientParameters ) -> AuthClient: """Authenticate against IBM Quantum and populate the hub/group/projects. @@ -432,7 +432,7 @@ def delete_account(name: Optional[str] = None) -> bool: True if the account was deleted. False if no account was found. """ - return AccountManager.delete(name=name, auth="legacy") + return AccountManager.delete(name=name, channel="ibm_quantum") @staticmethod def save_account( @@ -449,9 +449,8 @@ def save_account( Args: token: IBM Cloud API key or IBM Quantum API token. url: The API URL. - Defaults to https://cloud.ibm.com (cloud) or - https://auth.quantum-computing.ibm.com/api (legacy). - instance: The CRN (cloud) or hub/group/project (legacy). + Defaults to https://auth.quantum-computing.ibm.com/api + instance: The hub/group/project. name: Name of the account to save. proxies: Proxy configuration. Supported optional keys are ``urls`` (a dictionary mapping protocol or protocol and host to the URL of the proxy, @@ -466,7 +465,7 @@ def save_account( token=token, url=url, instance=instance, - auth="legacy", + channel="ibm_quantum", name=name, proxies=ProxyConfiguration(**proxies) if proxies else None, verify=verify, @@ -494,7 +493,9 @@ def saved_accounts( return dict( map( lambda kv: (kv[0], Account.to_saved_format(kv[1])), - AccountManager.list(default=default, auth="legacy", name=name).items(), + AccountManager.list( + default=default, channel="ibm_quantum", name=name + ).items(), ), ) diff --git a/test/account.py b/test/account.py index a8f4950f6..9851b7dd0 100644 --- a/test/account.py +++ b/test/account.py @@ -20,7 +20,7 @@ from unittest.mock import patch from qiskit_ibm_provider.accounts import management -from qiskit_ibm_provider.accounts.account import LEGACY_API_URL +from qiskit_ibm_provider.accounts.account import IBM_QUANTUM_API_URL class custom_envs(ContextDecorator): @@ -127,7 +127,7 @@ def __exit__(self, *exc): def get_account_config_contents( name=None, - auth="legacy", + channel="ibm_quantum", token=None, url=None, instance=None, @@ -136,26 +136,26 @@ def get_account_config_contents( ): """Generate qiskitrc content""" if instance is None: - instance = "some_instance" if auth == "cloud" else "hub/group/project" + instance = "some_instance" if channel == "ibm_cloud" else "hub/group/project" token = token or uuid.uuid4().hex if name is None: name = ( - management._DEFAULT_ACCOUNT_NAME_CLOUD - if auth == "cloud" - else management._DEFAULT_ACCOUNT_NAME_LEGACY + management._DEFAULT_ACCOUNT_NAME_IBM_CLOUD + if channel == "ibm_cloud" + else management._DEFAULT_ACCOUNT_NAME_IBM_QUANTUM ) if url is None: - url = LEGACY_API_URL + url = IBM_QUANTUM_API_URL out = { name: { - "auth": auth, + "channel": channel, "url": url, "token": token, "instance": instance, } } if verify is not None: - out["verify"] = verify + out[name]["verify"] = verify if proxies is not None: - out["proxies"] = proxies + out[name]["proxies"] = proxies return out diff --git a/test/unit/mock/fake_provider.py b/test/unit/mock/fake_provider.py index 1501e0513..2bb4ad513 100644 --- a/test/unit/mock/fake_provider.py +++ b/test/unit/mock/fake_provider.py @@ -35,7 +35,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def _authenticate_legacy_account( + def _authenticate_ibm_quantum_account( self, client_params: ClientParameters ) -> "FakeAuthClient": """Mock authentication.""" @@ -87,7 +87,10 @@ def __init__(self): # pylint: disable=super-init-not-called def current_service_urls(self): """Return service urls.""" - return {"http": "legacy_api_url", "services": {"runtime": "legacy_runtime_url"}} + return { + "http": "ibm_quantum_api_url", + "services": {"runtime": "ibm_quantum_runtime_url"}, + } def current_access_token(self): """Return access token.""" diff --git a/test/unit/test_account.py b/test/unit/test_account.py index 9811e7d1b..cccff6c12 100644 --- a/test/unit/test_account.py +++ b/test/unit/test_account.py @@ -21,12 +21,17 @@ from qiskit_ibm_provider.accounts import ( AccountManager, Account, - management, AccountAlreadyExistsError, AccountNotFoundError, InvalidAccountError, ) -from qiskit_ibm_provider.accounts.account import LEGACY_API_URL +from qiskit_ibm_provider.accounts.account import IBM_QUANTUM_API_URL +from qiskit_ibm_provider.accounts.management import ( + _DEFAULT_ACCOUNT_NAME_LEGACY, + _DEFAULT_ACCOUNT_NAME_CLOUD, + _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM, + _DEFAULT_ACCOUNT_NAME_IBM_CLOUD, +) from qiskit_ibm_provider.proxies import ProxyConfiguration from .mock.fake_provider import FakeProvider from ..account import ( @@ -37,15 +42,15 @@ ) from ..ibm_test_case import IBMTestCase -_TEST_LEGACY_ACCOUNT = Account( - auth="legacy", +_TEST_IBM_QUANTUM_ACCOUNT = Account( + channel="ibm_quantum", token="token-x", url="https://auth.quantum-computing.ibm.com/api", instance="ibm-q/open/main", ) -_TEST_CLOUD_ACCOUNT = Account( - auth="cloud", +_TEST_IBM_CLOUD_ACCOUNT = Account( + channel="ibm_cloud", token="token-y", url="https://cloud.ibm.com", instance="crn:v1:bluemix:public:quantum-computing:us-east:a/...::", @@ -54,23 +59,44 @@ ), ) +_TEST_LEGACY_ACCOUNT = { + "auth": "legacy", + "token": "token-x", + "url": "https://auth.quantum-computing.ibm.com/api", + "instance": "ibm-q/open/main", +} + +_TEST_CLOUD_ACCOUNT = { + "auth": "cloud", + "token": "token-y", + "url": "https://cloud.ibm.com", + "instance": "crn:v1:bluemix:public:quantum-computing:us-east:a/...::", + "proxies": { + "username_ntlm": "bla", + "password_ntlm": "blub", + "urls": {"https": "127.0.0.1"}, + }, +} + class TestAccount(IBMTestCase): """Tests for Account class.""" dummy_token = "123" - dummy_cloud_url = "https://us-east.quantum-computing.cloud.ibm.com" - dummy_legacy_url = "https://auth.quantum-computing.ibm.com/api" + dummy_ibm_cloud_url = "https://us-east.quantum-computing.cloud.ibm.com" + dummy_ibm_quantum_url = "https://auth.quantum-computing.ibm.com/api" - def test_invalid_auth(self): - """Test invalid values for auth parameter.""" + def test_invalid_channel(self): + """Test invalid values for channel parameter.""" with self.assertRaises(InvalidAccountError) as err: - invalid_auth: Any = "phantom" + invalid_channel: Any = "phantom" Account( - auth=invalid_auth, token=self.dummy_token, url=self.dummy_cloud_url + channel=invalid_channel, + token=self.dummy_token, + url=self.dummy_ibm_cloud_url, ).validate() - self.assertIn("Invalid `auth` value.", str(err.exception)) + self.assertIn("Invalid `channel` value.", str(err.exception)) def test_invalid_token(self): """Test invalid values for token parameter.""" @@ -80,7 +106,9 @@ def test_invalid_token(self): with self.subTest(token=token): with self.assertRaises(InvalidAccountError) as err: Account( - auth="cloud", token=token, url=self.dummy_cloud_url + channel="ibm_cloud", + token=token, + url=self.dummy_ibm_cloud_url, ).validate() self.assertIn("Invalid `token` value.", str(err.exception)) @@ -88,7 +116,7 @@ def test_invalid_url(self): """Test invalid values for url parameter.""" subtests = [ - {"auth": "cloud", "url": 123}, + {"channel": "ibm_cloud", "url": 123}, ] for params in subtests: with self.subTest(params=params): @@ -100,15 +128,15 @@ def test_invalid_instance(self): """Test invalid values for instance parameter.""" subtests = [ - {"auth": "cloud", "instance": ""}, - {"auth": "cloud"}, - {"auth": "legacy", "instance": "no-hgp-format"}, + {"channel": "ibm_cloud", "instance": ""}, + {"channel": "ibm_cloud"}, + {"channel": "ibm_quantum", "instance": "no-hgp-format"}, ] for params in subtests: with self.subTest(params=params): with self.assertRaises(InvalidAccountError) as err: Account( - **params, token=self.dummy_token, url=self.dummy_cloud_url + **params, token=self.dummy_token, url=self.dummy_ibm_cloud_url ).validate() self.assertIn("Invalid `instance` value.", str(err.exception)) @@ -131,9 +159,9 @@ def test_invalid_proxy_config(self): with self.assertRaises(ValueError) as err: Account( **params, - auth="legacy", + channel="ibm_quantum", token=self.dummy_token, - url=self.dummy_cloud_url, + url=self.dummy_ibm_cloud_url, ).validate() self.assertIn("Invalid proxy configuration", str(err.exception)) @@ -144,22 +172,99 @@ class TestAccountManager(IBMTestCase): """Tests for AccountManager class.""" @temporary_account_config_file( - contents={"conflict": _TEST_CLOUD_ACCOUNT.to_saved_format()} + contents={"conflict": _TEST_IBM_CLOUD_ACCOUNT.to_saved_format()} ) def test_save_without_override(self): """Test to override an existing account without setting overwrite=True.""" with self.assertRaises(AccountAlreadyExistsError): AccountManager.save( name="conflict", - token=_TEST_CLOUD_ACCOUNT.token, - url=_TEST_CLOUD_ACCOUNT.url, - instance=_TEST_CLOUD_ACCOUNT.instance, - auth="cloud", + token=_TEST_IBM_CLOUD_ACCOUNT.token, + url=_TEST_IBM_CLOUD_ACCOUNT.url, + instance=_TEST_IBM_CLOUD_ACCOUNT.instance, + channel="ibm_cloud", + overwrite=False, + ) + # TODO remove test when removing auth parameter + + @temporary_account_config_file( + contents={_DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_LEGACY_ACCOUNT} + ) + @no_envs(["QISKIT_IBM_TOKEN"]) + def test_save_channel_ibm_quantum_over_auth_legacy_without_overwrite(self): + """Test to overwrite an existing auth "legacy" account with channel "ibm_quantum" + and without setting overwrite=True.""" + with self.assertRaises(AccountAlreadyExistsError): + AccountManager.save( + token=_TEST_IBM_QUANTUM_ACCOUNT.token, + url=_TEST_IBM_QUANTUM_ACCOUNT.url, + instance=_TEST_IBM_QUANTUM_ACCOUNT.instance, + channel="ibm_quantum", + name=None, overwrite=False, ) + # TODO remove test when removing auth parameter @temporary_account_config_file( - contents={"conflict": _TEST_CLOUD_ACCOUNT.to_saved_format()} + contents={_DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_LEGACY_ACCOUNT} + ) + @no_envs(["QISKIT_IBM_TOKEN"]) + def test_save_channel_ibm_quantum_over_auth_legacy_with_overwrite(self): + """Test to overwrite an existing auth "elegacy" account with channel "ibm_quantum" + and with setting overwrite=True.""" + AccountManager.save( + token=_TEST_IBM_QUANTUM_ACCOUNT.token, + url=_TEST_IBM_QUANTUM_ACCOUNT.url, + instance=_TEST_IBM_QUANTUM_ACCOUNT.instance, + channel="ibm_quantum", + name=None, + overwrite=True, + ) + self.assertEqual( + _TEST_IBM_QUANTUM_ACCOUNT, AccountManager.get(channel="ibm_quantum") + ) + + # TODO remove test when removing auth parameter + @temporary_account_config_file( + contents={_DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT} + ) + @no_envs(["QISKIT_IBM_TOKEN"]) + def test_save_channel_ibm_cloud_over_auth_cloud_with_overwrite(self): + """Test to overwrite an existing auth "cloud" account with channel "ibm_cloud" + and with setting overwrite=True.""" + AccountManager.save( + token=_TEST_IBM_CLOUD_ACCOUNT.token, + url=_TEST_IBM_CLOUD_ACCOUNT.url, + instance=_TEST_IBM_CLOUD_ACCOUNT.instance, + channel="ibm_cloud", + proxies=_TEST_IBM_CLOUD_ACCOUNT.proxies, + name=None, + overwrite=True, + ) + self.assertEqual( + _TEST_IBM_CLOUD_ACCOUNT, AccountManager.get(channel="ibm_cloud") + ) + + # TODO remove test when removing auth parameter + @temporary_account_config_file(contents={"personal-account": _TEST_CLOUD_ACCOUNT}) + def test_save_channel_ibm_cloud_with_name_over_auth_cloud_with_overwrite(self): + """Test to overwrite an existing named auth "cloud" account with channel "ibm_cloud" + and with setting overwrite=True.""" + AccountManager.save( + token=_TEST_IBM_CLOUD_ACCOUNT.token, + url=_TEST_IBM_CLOUD_ACCOUNT.url, + instance=_TEST_IBM_CLOUD_ACCOUNT.instance, + channel="ibm_cloud", + proxies=_TEST_IBM_CLOUD_ACCOUNT.proxies, + name="personal-account", + overwrite=True, + ) + self.assertEqual( + _TEST_IBM_CLOUD_ACCOUNT, AccountManager.get(name="personal-account") + ) + + @temporary_account_config_file( + contents={"conflict": _TEST_IBM_CLOUD_ACCOUNT.to_saved_format()} ) def test_get_none(self): """Test to get an account with an invalid name.""" @@ -176,28 +281,31 @@ def test_save_get(self): # - the name passed to AccountManager.save # - the name passed to AccountManager.get sub_tests = [ - # verify accounts can be saved and retrieved via custom names - (_TEST_LEGACY_ACCOUNT, "acct-1", "acct-1"), - (_TEST_CLOUD_ACCOUNT, "acct-2", "acct-2"), - # verify default account name handling for cloud accounts - (_TEST_CLOUD_ACCOUNT, None, management._DEFAULT_ACCOUNT_NAME_CLOUD), - (_TEST_LEGACY_ACCOUNT, None, None), - # verify default account name handling for legacy accounts - (_TEST_LEGACY_ACCOUNT, None, management._DEFAULT_ACCOUNT_NAME_LEGACY), + (_TEST_IBM_QUANTUM_ACCOUNT, "acct-1", "acct-1"), + (_TEST_IBM_CLOUD_ACCOUNT, "acct-2", "acct-2"), + # verify default account name handling for ibm_cloud accounts + (_TEST_IBM_CLOUD_ACCOUNT, None, _DEFAULT_ACCOUNT_NAME_IBM_CLOUD), + (_TEST_IBM_CLOUD_ACCOUNT, None, None), + # verify default account name handling for ibm_quantum accounts + ( + _TEST_IBM_QUANTUM_ACCOUNT, + None, + _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM, + ), # verify account override - (_TEST_LEGACY_ACCOUNT, "acct", "acct"), - (_TEST_CLOUD_ACCOUNT, "acct", "acct"), + (_TEST_IBM_QUANTUM_ACCOUNT, "acct", "acct"), + (_TEST_IBM_CLOUD_ACCOUNT, "acct", "acct"), ] for account, name_save, name_get in sub_tests: with self.subTest( - f"for account type '{account.auth}' " + f"for account type '{account.channel}' " f"using `save(name={name_save})` and `get(name={name_get})`" ): AccountManager.save( token=account.token, url=account.url, instance=account.instance, - auth=account.auth, + channel=account.channel, proxies=account.proxies, verify=account.verify, name=name_save, @@ -208,8 +316,8 @@ def test_save_get(self): @temporary_account_config_file( contents=json.dumps( { - "cloud": _TEST_CLOUD_ACCOUNT.to_saved_format(), - "legacy": _TEST_LEGACY_ACCOUNT.to_saved_format(), + "ibm_cloud": _TEST_IBM_CLOUD_ACCOUNT.to_saved_format(), + "ibm_quantum": _TEST_IBM_QUANTUM_ACCOUNT.to_saved_format(), } ) ) @@ -218,15 +326,15 @@ def test_list(self): with temporary_account_config_file( contents={ - "key1": _TEST_CLOUD_ACCOUNT.to_saved_format(), - "key2": _TEST_LEGACY_ACCOUNT.to_saved_format(), + "key1": _TEST_IBM_CLOUD_ACCOUNT.to_saved_format(), + "key2": _TEST_IBM_QUANTUM_ACCOUNT.to_saved_format(), } ), self.subTest("non-empty list of accounts"): accounts = AccountManager.list() self.assertEqual(len(accounts), 2) - self.assertEqual(accounts["key1"], _TEST_CLOUD_ACCOUNT) - self.assertTrue(accounts["key2"], _TEST_LEGACY_ACCOUNT) + self.assertEqual(accounts["key1"], _TEST_IBM_CLOUD_ACCOUNT) + self.assertTrue(accounts["key2"], _TEST_IBM_QUANTUM_ACCOUNT) with temporary_account_config_file(contents={}), self.subTest( "empty list of accounts" @@ -235,33 +343,33 @@ def test_list(self): with temporary_account_config_file( contents={ - "key1": _TEST_CLOUD_ACCOUNT.to_saved_format(), - "key2": _TEST_LEGACY_ACCOUNT.to_saved_format(), - management._DEFAULT_ACCOUNT_NAME_CLOUD: Account( - "cloud", "token-cloud", instance="crn:123" + "key1": _TEST_IBM_CLOUD_ACCOUNT.to_saved_format(), + "key2": _TEST_IBM_QUANTUM_ACCOUNT.to_saved_format(), + _DEFAULT_ACCOUNT_NAME_IBM_CLOUD: Account( + "ibm_cloud", "token-ibm-cloud", instance="crn:123" ).to_saved_format(), - management._DEFAULT_ACCOUNT_NAME_LEGACY: Account( - "legacy", "token-legacy" + _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM: Account( + "ibm_quantum", "token-ibm-quantum" ).to_saved_format(), } ), self.subTest("filtered list of accounts"): - accounts = list(AccountManager.list(auth="cloud").keys()) + accounts = list(AccountManager.list(channel="ibm_cloud").keys()) self.assertEqual(len(accounts), 2) - self.assertListEqual( - accounts, ["key1", management._DEFAULT_ACCOUNT_NAME_CLOUD] - ) + self.assertListEqual(accounts, ["key1", _DEFAULT_ACCOUNT_NAME_IBM_CLOUD]) - accounts = list(AccountManager.list(auth="legacy").keys()) + accounts = list(AccountManager.list(channel="ibm_quantum").keys()) self.assertEqual(len(accounts), 2) - self.assertListEqual( - accounts, ["key2", management._DEFAULT_ACCOUNT_NAME_LEGACY] - ) + self.assertListEqual(accounts, ["key2", _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM]) - accounts = list(AccountManager.list(auth="cloud", default=True).keys()) + accounts = list( + AccountManager.list(channel="ibm_cloud", default=True).keys() + ) self.assertEqual(len(accounts), 1) - self.assertListEqual(accounts, [management._DEFAULT_ACCOUNT_NAME_CLOUD]) + self.assertListEqual(accounts, [_DEFAULT_ACCOUNT_NAME_IBM_CLOUD]) - accounts = list(AccountManager.list(auth="cloud", default=False).keys()) + accounts = list( + AccountManager.list(channel="ibm_cloud", default=False).keys() + ) self.assertEqual(len(accounts), 1) self.assertListEqual(accounts, ["key1"]) @@ -271,9 +379,9 @@ def test_list(self): @temporary_account_config_file( contents={ - "key1": _TEST_CLOUD_ACCOUNT.to_saved_format(), - management._DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_LEGACY_ACCOUNT.to_saved_format(), - management._DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT.to_saved_format(), + "key1": _TEST_IBM_CLOUD_ACCOUNT.to_saved_format(), + _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM: _TEST_IBM_QUANTUM_ACCOUNT.to_saved_format(), + _DEFAULT_ACCOUNT_NAME_IBM_CLOUD: _TEST_IBM_CLOUD_ACCOUNT.to_saved_format(), } ) def test_delete(self): @@ -283,10 +391,32 @@ def test_delete(self): self.assertTrue(AccountManager.delete(name="key1")) self.assertFalse(AccountManager.delete(name="key1")) - with self.subTest("delete default legacy account"): - self.assertTrue(AccountManager.delete(auth="legacy")) + with self.subTest("delete default ibm_quantum account"): + self.assertTrue(AccountManager.delete(channel="ibm_quantum")) - with self.subTest("delete default cloud account"): + with self.subTest("delete default ibm_cloud account"): + self.assertTrue(AccountManager.delete()) + + self.assertTrue(len(AccountManager.list()) == 0) + + @temporary_account_config_file( + contents={ + "key1": _TEST_CLOUD_ACCOUNT, + _DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_LEGACY_ACCOUNT, + _DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT, + } + ) + def test_delete_auth(self): + """Test delete accounts already saved using auth.""" + + with self.subTest("delete named account"): + self.assertTrue(AccountManager.delete(name="key1")) + self.assertFalse(AccountManager.delete(name="key1")) + + with self.subTest("delete default auth='legacy' account using channel"): + self.assertTrue(AccountManager.delete(channel="ibm_quantum")) + + with self.subTest("delete default auth='cloud' account using channel"): self.assertTrue(AccountManager.delete()) self.assertTrue(len(AccountManager.list()) == 0) @@ -349,19 +479,8 @@ def test_enable_account_by_name_and_other(self): self.assertEqual(service._account.token, token) self.assertIn("are ignored", logged.output[0]) - def test_enable_legacy_account_by_auth_token_url(self): - """Test initializing legacy account by auth, token, url.""" - urls = [(None, LEGACY_API_URL), ("some_url", "some_url")] - for url, expected in urls: - with self.subTest(url=url), no_envs(["QISKIT_IBM_TOKEN"]): - token = uuid.uuid4().hex - service = FakeProvider(token=token, url=url) - self.assertTrue(service._account) - self.assertEqual(service._account.token, token) - self.assertEqual(service._account.url, expected) - - def test_enable_account_by_auth_url(self): - """Test initializing legacy account by token, url.""" + def test_enable_account_by_token_url_2(self): + """Test initializing ibm quantum account by token, url.""" token = uuid.uuid4().hex with temporary_account_config_file(token=token), no_envs(["QISKIT_IBM_TOKEN"]): @@ -370,34 +489,36 @@ def test_enable_account_by_auth_url(self): self.assertTrue(service._account) self.assertEqual(service._account.token, token) - expected = LEGACY_API_URL + expected = IBM_QUANTUM_API_URL self.assertEqual(service._account.url, expected) self.assertIn("url", logged.output[0]) - def test_enable_account_by_only_auth(self): + def test_enable_account_by_only_channel(self): """Test initializing account with single saved account.""" token = uuid.uuid4().hex with temporary_account_config_file(token=token), no_envs(["QISKIT_IBM_TOKEN"]): service = FakeProvider() self.assertTrue(service._account) self.assertEqual(service._account.token, token) - expected = LEGACY_API_URL + expected = IBM_QUANTUM_API_URL self.assertEqual(service._account.url, expected) - self.assertEqual(service._account.auth, "legacy") + self.assertEqual(service._account.channel, "ibm_quantum") - def test_enable_account_both_auth(self): + def test_enable_account_both_channel(self): """Test initializing account with both saved types.""" token = uuid.uuid4().hex - contents = get_account_config_contents(auth="cloud", token=uuid.uuid4().hex) - contents.update(get_account_config_contents(auth="legacy", token=token)) + contents = get_account_config_contents( + channel="ibm_cloud", token=uuid.uuid4().hex + ) + contents.update(get_account_config_contents(channel="ibm_quantum", token=token)) with temporary_account_config_file(contents=contents), no_envs( ["QISKIT_IBM_TOKEN"] ): service = FakeProvider() self.assertTrue(service._account) self.assertEqual(service._account.token, token) - self.assertEqual(service._account.url, LEGACY_API_URL) - self.assertEqual(service._account.auth, "legacy") + self.assertEqual(service._account.url, IBM_QUANTUM_API_URL) + self.assertEqual(service._account.channel, "ibm_quantum") def test_enable_account_by_env_auth(self): """Test initializing account by environment variable and auth.""" @@ -415,7 +536,7 @@ def test_enable_account_by_env_auth(self): self.assertTrue(service._account) self.assertEqual(service._account.token, token) self.assertEqual(service._account.url, url) - self.assertEqual(service._account.auth, "legacy") + self.assertEqual(service._account.channel, "ibm_quantum") def test_enable_account_bad_name(self): """Test initializing account by bad name.""" @@ -437,9 +558,7 @@ def test_enable_account_by_name_pref(self): ] for extra in subtests: with self.subTest(extra=extra): - with temporary_account_config_file( - name=name, verify=True, proxies="some proxies" - ): + with temporary_account_config_file(name=name, verify=True, proxies={}): service = FakeProvider(name=name, **extra) self.assertTrue(service._account) self._verify_prefs(extra, service._account)