Skip to content

Commit

Permalink
Merge branch 'main' into enable-test-provider
Browse files Browse the repository at this point in the history
  • Loading branch information
kt474 committed Mar 31, 2022
2 parents 462a9a1 + 7aeef38 commit cf371f6
Show file tree
Hide file tree
Showing 16 changed files with 380 additions and 216 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ integration-test:
python -m unittest -v test/integration/test_backend.py test/integration/test_account_client.py test/integration/test_filter_backends.py \
test/integration/test_serialization.py test/integration/test_ibm_job_attributes.py test/integration/test_basic_server_paths.py \
test/integration/test_ibm_integration.py test/integration/test_ibm_job.py test/integration/test_ibm_qasm_simulator.py \
test/integration/test_proxies.py test/integration/test_ibm_provider.py
test/integration/test_proxies.py test/integration/test_ibm_provider.py test/integration/test_composite_job.py

black:
black qiskit_ibm_provider test setup.py docs/tutorials
2 changes: 1 addition & 1 deletion qiskit_ibm_provider/accounts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
38 changes: 20 additions & 18 deletions qiskit_ibm_provider/accounts/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)


Expand All @@ -35,7 +36,7 @@ class Account:

def __init__(
self,
auth: AccountType,
channel: ChannelType,
token: str,
url: Optional[str] = None,
instance: Optional[str] = None,
Expand All @@ -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
Expand All @@ -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"),
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
116 changes: 82 additions & 34 deletions qiskit_ibm_provider/accounts/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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,
)
Expand All @@ -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
Expand All @@ -91,15 +95,18 @@ 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(),
)

# filter based on input parameters
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,
Expand All @@ -111,20 +118,21 @@ 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.
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
Expand All @@ -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])

Expand All @@ -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
)
1 change: 0 additions & 1 deletion qiskit_ibm_provider/accounts/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ def read_config(
return data
if name in data:
return data[name]

return None


Expand Down
Loading

0 comments on commit cf371f6

Please sign in to comment.