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
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ indent-after-paren=4
indent-string=' '

# Maximum number of characters on a single line.
max-line-length=105
max-line-length=120
daka1510 marked this conversation as resolved.
Show resolved Hide resolved

# Maximum number of lines in a module.
max-module-lines=1000
Expand Down
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
102 changes: 102 additions & 0 deletions qiskit_ibm_runtime/accounts/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 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


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

def __init__(
self,
auth: AccountType,
token: str,
url: Optional[str],
instance: Optional[str] = None,
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:
"""Read account from disk."""
daka1510 marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Up @@ -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
Expand All @@ -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]

Expand Down
Loading