Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Allow setting of API key through CLI
 - Add function to set any field in the configuration file
 - Add function to read out the configuration file
 - Towards full configurability from CLI
  • Loading branch information
PGijsbers committed Apr 8, 2021
commit 0f812f2f869127d69cbbd4806dcaaed706c6dfd3
108 changes: 103 additions & 5 deletions openml/cli.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,95 @@
"""" Command Line Interface for `openml` to configure its settings. """

import argparse
import string
from typing import Union, Callable

# from openml import config

from openml import config


def is_hex(string_: str) -> bool:
return all(c in string.hexdigits for c in string_)


def looks_like_api_key(apikey: str) -> bool:
return len(apikey) == 32 and is_hex(apikey)


def wait_until_valid_input(
prompt: str,
check: Callable[[str], bool],
error_message: Union[str, Callable[[str], str]] = "That is not a valid response.",
) -> str:
""" Asks `prompt` until an input is received which returns True for `check`.

Parameters
----------
prompt: str
message to display
check: Callable[[str], bool]
function to call with the given input, should return true only if the input is valid.
error_message: Union[str, Callable[[str], str]
a message to display on invalid input, or a `str`->`str` function that can give feedback
specific to the error.

Returns
-------

"""

response = input(prompt)
while not check(response):
if isinstance(error_message, str):
print(error_message)
else:
print(error_message(response), end="\n\n")
response = input(prompt)

return response


def print_configuration():
file = config.determine_config_file_path()
header = f"File '{file}' contains (or defaults to):"
print(header)

max_key_length = max(map(len, config.get_config_as_dict()))
for field, value in config.get_config_as_dict().items():
print(f"{field.ljust(max_key_length)}: {value}")


def configure_apikey() -> None:
print(f"\nYour current API key is set to: '{config.apikey}'")
print("You can get an API key at https://new.openml.org")
print("You must create an account if you don't have one yet.")
print(" 1. Log in with the account.")
print(" 2. Navigate to the profile page (top right circle > Your Profile). ")
print(" 3. Click the API Key button to reach the page with your API key.")
print("If you have any difficulty following these instructions, please let us know on Github.")

def apikey_error(apikey: str) -> str:
if len(apikey) != 32:
return f"The key should contain 32 characters but contains {len(apikey)}."
if not is_hex(apikey):
return "Some characters are not hexadecimal."
return "This does not look like an API key."

response = wait_until_valid_input(
prompt="Please enter your API key:", check=looks_like_api_key, error_message=apikey_error,
)

config.set_field_in_config_file("apikey", response)
print("Key set.")


def configure(args: argparse.Namespace):
""" Configures the openml configuration file. """
print("Configuring", args.file)

# check if API key exists, if so ask to overwrite.
print_configuration()
set_functions = {
"apikey": configure_apikey,
}
set_functions.get(args.field, quit)()


def main() -> None:
Expand All @@ -21,8 +101,26 @@ def main() -> None:
parser_configure = subparsers.add_parser(
"configure", description="Set or read variables in your configuration file.",
)

parser_configure.add_argument(
"file", default="~/.openml/config", help="The configuration file to edit or read."
"field",
type=str,
choices=[
"apikey",
"server",
"cachedir",
"avoid_duplicate_runs",
"connection_n_retries",
"verbosity",
"all",
"none",
],
default="all",
nargs="?",
help="The field you wish to edit, auto-completes the field name, "
"e.g. `openml configure cache` is equivalent to `openml configure cachedir`."
"Choosing 'all' lets you configure all fields one by one."
"Choosing 'none' will print out the current configuration.",
)

args = parser.parse_args()
Expand Down
47 changes: 39 additions & 8 deletions openml/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import os
from pathlib import Path
import platform
from typing import Tuple, cast
from typing import Tuple, cast, Any

from io import StringIO
import configparser
Expand Down Expand Up @@ -177,6 +177,16 @@ def stop_using_configuration_for_example(cls):
cls._start_last_called = False


def determine_config_file_path() -> Path:
if platform.system() == "Linux":
config_dir = Path(os.environ.get("XDG_CONFIG_HOME", Path("~") / ".config" / "openml"))
else:
config_dir = Path("~") / ".openml"
# Still use os.path.expanduser to trigger the mock in the unit test
config_dir = Path(os.path.expanduser(config_dir))
return config_dir / "config"


def _setup(config=None):
"""Setup openml package. Called on first import.

Expand All @@ -193,13 +203,8 @@ def _setup(config=None):
global connection_n_retries
global max_retries

if platform.system() == "Linux":
config_dir = Path(os.environ.get("XDG_CONFIG_HOME", Path("~") / ".config" / "openml"))
else:
config_dir = Path("~") / ".openml"
# Still use os.path.expanduser to trigger the mock in the unit test
config_dir = Path(os.path.expanduser(config_dir))
config_file = config_dir / "config"
config_file = determine_config_file_path()
config_dir = config_file.parent

# read config file, create directory for config file
if not os.path.exists(config_dir):
Expand Down Expand Up @@ -258,6 +263,32 @@ def _get(config, key):
)


def set_field_in_config_file(field: str, value: Any):
""" Overwrites the `field` in the configuration file with the new `value`. """
fields = [
"apikey",
"server",
"cache_directory",
"avoid_duplicate_runs",
"connection_n_retries",
"max_retries",
]
if field not in fields:
return ValueError(f"Field '{field}' is not valid and must be one of '{fields}'.")

globals()[field] = value
config_file = determine_config_file_path()
config = _parse_config(str(config_file))
with open(config_file, "w") as fh:
for f in fields:
# We can't blindly set all values based on globals() because the user when the user
# sets it through config.FIELD it should not be stored to file.
value = config.get("FAKE_SECTION", f)
if f == field:
value = globals()[f]
fh.write(f"{f} = {value}\n")


def _parse_config(config_file: str):
""" Parse the config file, set up defaults. """
config = configparser.RawConfigParser(defaults=_defaults)
Expand Down