Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add account management functionality #49

Merged
merged 11 commits into from
Dec 15, 2021
18 changes: 18 additions & 0 deletions qiskit_ibm_runtime/accounts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 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.

"""
Account management functionality related to the IBM Runtime Services.
"""

from .account import Account, AccountType
from .management import AccountManager
104 changes: 104 additions & 0 deletions qiskit_ibm_runtime/accounts/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# 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.

"""Account related classes and functions."""


from typing import Optional
from urllib.parse import urlparse

from typing_extensions import Literal

AccountType = Optional[Literal["cloud", "legacy"]]

LEGACY_API_URL = "https://auth.quantum-computing.ibm.com/api"
CLOUD_API_URL = "https://cloud.ibm.com"


def _assert_valid_auth(auth: AccountType) -> None:
"""Assert that the auth parameter is valid."""
if not (auth in ["cloud", "legacy"]):
raise ValueError(
f"Inappropriate `auth` value. Expected one of ['cloud', 'legacy'], got '{auth}'."
)


def _assert_valid_token(token: str) -> None:
"""Assert that the token is valid."""
if not (isinstance(token, str) and len(token) > 0):
raise ValueError(
f"Inappropriate `token` value. Expected a non-empty string, got '{token}'."
)


def _assert_valid_url(url: str) -> None:
"""Assert that the URL is valid."""
try:
urlparse(url)
except:
raise ValueError(f"Inappropriate `url` value. Failed to parse '{url}' as URL.")


def _assert_valid_instance(auth: AccountType, instance: str) -> None:
"""Assert that the instance name is valid for the given account type."""
if auth == "cloud":
if not (isinstance(instance, str) and len(instance) > 0):
raise ValueError(
f"Inappropriate `instance` value. Expected a non-empty string."
)
daka1510 marked this conversation as resolved.
Show resolved Hide resolved
# TODO: add validation for legacy instance when adding test coverage


class Account:
"""Class that represents an account."""

def __init__(
self,
auth: AccountType,
token: str,
url: Optional[str],
instance: Optional[str] = None,
# TODO: add validation for proxies input format
proxies: Optional[dict] = None,
verify: Optional[bool] = True,
):
"""Account constructor."""
_assert_valid_auth(auth)
self.auth = auth

_assert_valid_token(token)
self.token = token

resolved_url = url or LEGACY_API_URL if auth == "legacy" else CLOUD_API_URL
_assert_valid_url(resolved_url)
self.url = resolved_url

_assert_valid_instance(auth, instance)
self.instance = instance
self.proxies = proxies
daka1510 marked this conversation as resolved.
Show resolved Hide resolved
self.verify = verify

def to_saved_format(self) -> dict:
"""Returns a dictionary that represents how the account is saved on disk."""
return {k: v for k, v in self.__dict__.items() if v is not None}

@classmethod
def from_saved_format(cls, data: dict) -> "Account":
"""Creates an account instance from data saved on disk."""
return cls(
auth=data.get("auth"),
url=data.get("url"),
token=data.get("token"),
instance=data.get("instance"),
proxies=data.get("proxies"),
verify=data.get("verify"),
)
19 changes: 19 additions & 0 deletions qiskit_ibm_runtime/accounts/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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.

"""Exceptions for the ``Accounts`` module."""

from ..exceptions import IBMError


class AccountsError(IBMError):
"""Base class for errors raised during account management."""
71 changes: 71 additions & 0 deletions qiskit_ibm_runtime/accounts/management.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# 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.

"""Account management related classes and functions."""

import os
from typing import Optional, Union

from .account import Account, AccountType
from .storage import save_config, read_config, delete_config

_DEFAULT_ACCOUNG_CONFIG_JSON_FILE = os.path.join(
os.path.expanduser("~"), ".qiskit", "qiskit-ibm.json"
)
_DEFAULT_ACCOUNT_NAME = "default"


class AccountManager:
"""Class that bundles account management related functionality."""

@staticmethod
def save(
token: Optional[str] = None,
url: Optional[str] = None,
instance: Optional[str] = None,
auth: Optional[AccountType] = None,
name: Optional[str] = _DEFAULT_ACCOUNT_NAME,
proxies: Optional[dict] = None,
verify: Optional[bool] = None,
) -> None:
"""Save account on disk."""

return save_config(
filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE,
name=name,
config=Account(
token=token,
url=url,
instance=instance,
auth=auth,
proxies=proxies,
verify=verify,
).to_saved_format(),
)

@staticmethod
def list() -> Union[dict, None]:
"""List all accounts saved on disk."""

return read_config(filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE)

@staticmethod
def get(name: Optional[str] = _DEFAULT_ACCOUNT_NAME) -> Account:
"""Read account from disk."""
return Account.from_saved_format(
read_config(filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE, name=name)
)

@staticmethod
def delete(name: Optional[str] = _DEFAULT_ACCOUNT_NAME) -> bool:
"""Delete account from disk."""
return delete_config(name=name, filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE)
82 changes: 82 additions & 0 deletions qiskit_ibm_runtime/accounts/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 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.

"""Utility functions related to storing account configuration on disk."""

import json
import os
from typing import Optional, Union


def save_config(
filename: str,
name: str,
config: dict,
) -> None:
"""Save configuration data in a JSON file under the given name."""

_ensure_file_exists(filename)

with open(filename, mode="r") as json_in:
data = json.load(json_in)

with open(filename, mode="w") as json_out:
data[name] = config
json.dump(data, json_out, sort_keys=True, indent=4)


def read_config(
filename: str,
name: Optional[str] = None,
) -> Union[dict, None]:
"""Save configuration data from a JSON file."""

_ensure_file_exists(filename)

with open(filename) as json_file:
data = json.load(json_file)
if name is None:
return data
if name in data:
return data[name]

return None


def delete_config(
filename: str,
name: str,
) -> bool:
"""Delete configuration data from a JSON file."""

_ensure_file_exists(filename)
with open(filename, mode="r") as json_in:
data = json.load(json_in)

if name in data:
with open(filename, mode="w") as json_out:
del data[name]
json.dump(data, json_out, sort_keys=True, indent=4)
return True

return False


def _ensure_file_exists(filename: str, initial_content: str = "{}") -> None:
if not os.path.isfile(filename):

# create parent directories
os.makedirs(os.path.dirname(filename), exist_ok=True)

# initialize file
with open(filename, mode="w") as json_file:
json_file.write(initial_content)
32 changes: 6 additions & 26 deletions qiskit_ibm_runtime/credentials/__init__.py
Original file line number Diff line number Diff line change
@@ -32,47 +32,31 @@
.. autosummary::
:toctree: ../stubs/

CredentialsError
InvalidCredentialsFormatError
CredentialsNotFoundError

"""

from collections import OrderedDict
from typing import Dict, Optional, Tuple, Any
import logging
from collections import OrderedDict
from typing import Dict, Tuple, Any

from .credentials import Credentials
from .hub_group_project_id import HubGroupProjectID
from .environ import read_credentials_from_environ
from .exceptions import (
CredentialsError,
InvalidCredentialsFormatError,
CredentialsNotFoundError,
HubGroupProjectIDInvalidStateError,
)
from .configrc import (
read_credentials_from_qiskitrc,
store_credentials,
store_preferences,
)
from .environ import read_credentials_from_environ
from .hub_group_project_id import HubGroupProjectID

logger = logging.getLogger(__name__)


def discover_credentials(
qiskitrc_filename: Optional[str] = None,
) -> Tuple[Dict[HubGroupProjectID, Credentials], Dict]:
def discover_credentials() -> Tuple[Dict[HubGroupProjectID, Credentials], Dict]:
"""Automatically discover credentials for IBM Quantum.

This method looks for credentials in the following places in order and
returns the first ones found:
daka1510 marked this conversation as resolved.
Show resolved Hide resolved

1. The environment variables.
2. The ``qiskitrc`` configuration file

Args:
qiskitrc_filename: Full path to the ``qiskitrc`` configuration
file. If ``None``, ``$HOME/.qiskitrc/qiskitrc`` is used.

Raises:
HubGroupProjectIDInvalidStateError: If the default provider stored on
@@ -92,10 +76,6 @@ def discover_credentials(
readers = OrderedDict(
[
("environment variables", (read_credentials_from_environ, {})),
(
"qiskitrc",
(read_credentials_from_qiskitrc, {"filename": qiskitrc_filename}),
),
]
) # type: OrderedDict[str, Any]

Loading