Skip to content

Commit

Permalink
Allow user to define a default account as an environment variable (#1018
Browse files Browse the repository at this point in the history
)

* Allow user to define a default account as an environment variable

* Fixed test

* Fixed mistaken paste

* Cleaned up test

* Moved test to TestAccountManager

* Added ability to define default channel in save_account

* Cleaned up code, fixed bugs

* Changed name of parameter

* Added test. Cleaned up code surrounding preferences of channel selection

* black and lint

* Fixed bug when json file was empty

* Code cleanup and documentation

* Documentation

* Removed channel from condition, because unnecessary

* changed default_channel to default_account

* Changed saving and getting default channel to default account

* black

* Documentation

* Release notes

* Reverted diff that was unnecessary

---------

Co-authored-by: Kevin Tian <[email protected]>
  • Loading branch information
merav-aharoni and kt474 authored Sep 19, 2023
1 parent 342ba6b commit ed3f533
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 16 deletions.
67 changes: 57 additions & 10 deletions qiskit_ibm_runtime/accounts/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def save(
verify: Optional[bool] = None,
overwrite: Optional[bool] = False,
channel_strategy: Optional[str] = None,
set_as_default: Optional[bool] = None,
) -> None:
"""Save account on disk."""
channel = channel or os.getenv("QISKIT_IBM_CHANNEL") or _DEFAULT_CHANNEL_TYPE
Expand All @@ -69,6 +70,7 @@ def save(
)
# avoid storing invalid accounts
.validate().to_saved_format(),
set_as_default=set_as_default,
)

@staticmethod
Expand Down Expand Up @@ -137,6 +139,21 @@ def get(
filename: Full path of the file from which to get the account.
name: Account name.
channel: Channel type.
Order of precedence for selecting the account:
1. If name is specified, get account with that name
2. If the environment variables define an account, get that one
3. If the channel parameter is defined,
a. get the account of this channel type defined as "is_default_account"
b. get the account of this channel type with default name
c. get any account of this channel type
4. If the channel is defined in "QISKIT_IBM_CHANNEL"
a. get the account of this channel type defined as "is_default_account"
b. get the account of this channel type with default name
c. get any account of this channel type
5. If a default account is defined in the json file, get that account
6. Get any account that is defined in the json file with
preference for _DEFAULT_CHANNEL_TYPE.
Returns:
Account information.
Expand All @@ -157,18 +174,20 @@ def get(
if env_account is not None:
return env_account

if channel:
saved_account = read_config(
filename=filename,
name=cls._get_default_account_name(channel=channel),
)
if saved_account is None:
if os.path.isfile(_QISKITRC_CONFIG_FILE):
return cls._from_qiskitrc_file()
raise AccountNotFoundError(f"No default {channel} account saved.")
all_config = read_config(filename=filename)
# Get the default account for the given channel.
# If channel == None, get the default account, for any channel, if it exists
saved_account = cls._get_default_account(all_config, channel)

if saved_account is not None:
return Account.from_saved_format(saved_account)

all_config = read_config(filename=filename)
# Get the default account from the channel defined in the environment variable
account = cls._get_default_account(all_config, channel=channel_)
if account is not None:
return Account.from_saved_format(account)

# check for any account
for channel_type in _CHANNEL_TYPES:
account_name = cls._get_default_account_name(channel=channel_type)
if account_name in all_config:
Expand Down Expand Up @@ -209,6 +228,34 @@ def _from_env_variables(cls, channel: Optional[ChannelType]) -> Optional[Account
channel=channel,
)

@classmethod
def _get_default_account(
cls, all_config: dict, channel: Optional[str] = None
) -> Optional[dict]:
default_channel_account = None
any_channel_account = None

for account_name in all_config:
account = all_config[account_name]
if channel:
if account.get("channel") == channel and account.get("is_default_account"):
return account
if account.get(
"channel"
) == channel and account_name == cls._get_default_account_name(channel):
default_channel_account = account
if account.get("channel") == channel:
any_channel_account = account
else:
if account.get("is_default_account"):
return account

if default_channel_account:
return default_channel_account
elif any_channel_account:
return any_channel_account
return None

@classmethod
def _get_default_account_name(cls, channel: ChannelType) -> str:
return (
Expand Down
22 changes: 20 additions & 2 deletions qiskit_ibm_runtime/accounts/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
logger = logging.getLogger(__name__)


def save_config(filename: str, name: str, config: dict, overwrite: bool) -> None:
def save_config(
filename: str, name: str, config: dict, overwrite: bool, set_as_default: Optional[bool] = None
) -> None:
"""Save configuration data in a JSON file under the given name."""
logger.debug("Save configuration data for '%s' in '%s'", name, filename)
_ensure_file_exists(filename)
Expand All @@ -35,8 +37,24 @@ def save_config(filename: str, name: str, config: dict, overwrite: bool) -> None
f"Named account ({name}) already exists. " f"Set overwrite=True to overwrite."
)

data[name] = config

# if set_as_default, but another account is defined as default, user must specify overwrite to change
# the default account.
if set_as_default:
data[name]["is_default_account"] = True
for account_name in data:
account = data[account_name]
if account_name != name and account.get("is_default_account"):
if overwrite:
del account["is_default_account"]
else:
raise AccountAlreadyExistsError(
f"default_account ({name}) already exists. "
f"Set overwrite=True to overwrite."
)

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


Expand Down
6 changes: 6 additions & 0 deletions qiskit_ibm_runtime/qiskit_runtime_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def __init__(
- Account with the input `name`, if specified.
- Default account for the `channel` type, if `channel` is specified but `token` is not.
- Account defined by the input `channel` and `token`, if specified.
- Account defined by the `default_channel` if defined in filename
- Account defined by the environment variables, if defined.
- Default account for the ``ibm_cloud`` account, if one is available.
- Default account for the ``ibm_quantum`` account, if one is available.
Expand Down Expand Up @@ -287,6 +288,7 @@ def _discover_account(
"'channel' is required if 'token', or 'url' is specified but 'name' is not."
)

# channel is not defined yet, get it from the AccountManager
if account is None:
account = AccountManager.get(filename=filename)

Expand Down Expand Up @@ -689,6 +691,7 @@ def save_account(
verify: Optional[bool] = None,
overwrite: Optional[bool] = False,
channel_strategy: Optional[str] = None,
set_as_default: Optional[bool] = None,
) -> None:
"""Save the account to disk for future use.
Expand All @@ -709,6 +712,8 @@ def save_account(
verify: Verify the server's TLS certificate.
overwrite: ``True`` if the existing account is to be overwritten.
channel_strategy: Error mitigation strategy.
set_as_default: If ``True``, the account is saved in filename,
as the default account.
"""

AccountManager.save(
Expand All @@ -722,6 +727,7 @@ def save_account(
verify=verify,
overwrite=overwrite,
channel_strategy=channel_strategy,
set_as_default=set_as_default,
)

@staticmethod
Expand Down
6 changes: 6 additions & 0 deletions releasenotes/notes/default_account-13d86d50f5b1d972.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
features:
- |
Added the option to define a default account in the account json file.
The select an account as default, define ``set_as_default=True`` in
``QiskitRuntimeService.save_account()``.
3 changes: 3 additions & 0 deletions test/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def get_account_config_contents(
instance=None,
verify=None,
proxies=None,
set_default=None,
):
"""Generate qiskitrc content"""
if instance is None:
Expand All @@ -177,4 +178,6 @@ def get_account_config_contents(
out[name]["verify"] = verify
if proxies is not None:
out[name]["proxies"] = proxies
if set_default:
out[name]["is_default_account"] = True
return out
Loading

0 comments on commit ed3f533

Please sign in to comment.