Skip to content

Commit

Permalink
feat(svc): add endpoints for renku config
Browse files Browse the repository at this point in the history
  • Loading branch information
Panaetius committed Jan 13, 2021
1 parent 2927019 commit 07973c6
Show file tree
Hide file tree
Showing 16 changed files with 541 additions and 58 deletions.
5 changes: 3 additions & 2 deletions conftest.py
Expand Up @@ -359,14 +359,15 @@ def project_metadata(project):
def client(project):
"""Return a Renku repository."""
from renku.core.management import LocalClient
from renku.core.management.config import ConfigFilter

original_get_value = LocalClient.get_value

def mocked_get_value(self, section, key, local_only=False, global_only=False):
def mocked_get_value(self, section, key, config_filter=ConfigFilter.ALL):
"""We don't want lfs warnings in tests."""
if key == "show_lfs_message":
return "False"
return original_get_value(self, section, key, local_only, global_only)
return original_get_value(self, section, key, config_filter=config_filter)

LocalClient.get_value = mocked_get_value

Expand Down
50 changes: 39 additions & 11 deletions renku/cli/config.py
Expand Up @@ -103,8 +103,8 @@
"""
import click

from renku.core import errors
from renku.core.commands.config import read_config, update_config
from renku.cli.utils.click import MutuallyExclusiveOption
from renku.core.commands.config import ConfigFilter, read_config, update_config


@click.group()
Expand All @@ -115,18 +115,46 @@ def config():

@config.command()
@click.argument("key", required=False, default=None)
@click.option("--local", "local_only", is_flag=True, help="Read from local configuration only.")
@click.option("--global", "global_only", is_flag=True, help="Read from global configuration only.")
def show(key, local_only, global_only):
@click.option(
"--local",
"local_only",
cls=MutuallyExclusiveOption,
is_flag=True,
help="Read from local configuration only.",
mutually_exclusive=["global_only", "default_only"],
)
@click.option(
"--global",
"global_only",
cls=MutuallyExclusiveOption,
is_flag=True,
help="Read from global configuration only.",
mutually_exclusive=["local_only", "default_only"],
)
@click.option(
"--default",
"default_only",
cls=MutuallyExclusiveOption,
is_flag=True,
help="Show default values if applicable.",
mutually_exclusive=["local_only", "global_only"],
)
def show(key, local_only, global_only, default_only):
"""Show current configuration.
KEY is of the form <group>.<entry>, e.g. 'interactive.default_url'.
"""
if local_only and global_only:
raise errors.UsageError("Cannot use --local and --global together.")
config_filter = ConfigFilter.ALL

value = read_config(key, local_only, global_only)
click.secho(value)
if local_only:
config_filter = ConfigFilter.LOCAL_ONLY
elif global_only:
config_filter = ConfigFilter.GLOBAL_ONLY
elif default_only:
config_filter = ConfigFilter.DEFAULT_ONLY

value = read_config().build().execute(key, config_filter=config_filter)
click.secho(value.output)


@config.command("set")
Expand All @@ -138,7 +166,7 @@ def set_(key, value, global_only):
KEY is of the form <group>.<entry>, e.g. 'interactive.default_url'.
"""
update_config(key, value=value, global_only=global_only)
update_config().build().execute(key, value=value, global_only=global_only)
click.secho("OK", fg="green")


Expand All @@ -150,5 +178,5 @@ def remove(key, global_only):
KEY is of the form <group>.<entry>, e.g. 'interactive.default_url'.
"""
update_config(key, remove=True, global_only=global_only)
update_config().build().execute(key, remove=True, global_only=global_only)
click.secho("OK", fg="green")
22 changes: 22 additions & 0 deletions renku/cli/utils/click.py
Expand Up @@ -31,3 +31,25 @@ def convert(self, value, param, ctx):
if value is None:
return None
return super(CaseInsensitiveChoice, self).convert(value.lower(), param, ctx)


class MutuallyExclusiveOption(click.Option):
"""Custom option class to allow specifying mutually exclusive options in click commands."""

def __init__(self, *args, **kwargs):
self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", []))
help = kwargs.get("help", "")
if self.mutually_exclusive:
ex_str = ", ".join(self.mutually_exclusive)
kwargs["help"] = help + (" NOTE: This argument is mutually exclusive with " " arguments: [" + ex_str + "].")
super(MutuallyExclusiveOption, self).__init__(*args, **kwargs)

def handle_parse_result(self, ctx, opts, args):
"""Handles the parse result for the option."""
if self.mutually_exclusive.intersection(opts) and self.name in opts:
raise click.UsageError(
"Illegal usage: `{}` is mutually exclusive with "
"arguments `{}`.".format(self.name, ", ".join(self.mutually_exclusive))
)

return super(MutuallyExclusiveOption, self).handle_parse_result(ctx, opts, args)
51 changes: 41 additions & 10 deletions renku/core/commands/config.py
Expand Up @@ -17,21 +17,38 @@
# limitations under the License.
"""Get and set Renku repository or global options."""
from renku.core import errors
from renku.core.management.config import CONFIG_LOCAL_PATH

from .client import pass_local_client
from renku.core.incubation.command import Command
from renku.core.management.config import CONFIG_LOCAL_PATH, ConfigFilter


def _split_section_and_key(key):
"""Return a tuple with config section and key."""
parts = key.split(".")
if len(parts) > 1:
return 'renku "{0}"'.format(parts[0]), ".".join(parts[1:])
return "{0}".format(parts[0]), ".".join(parts[1:])
return "renku", key


@pass_local_client(clean=False, requires_migration=True, commit=True, commit_only=CONFIG_LOCAL_PATH, commit_empty=False)
def update_config(client, key, *, value=None, remove=False, global_only=False, commit_message=None):
def _update_multiple_config(client, values, global_only=False, commit_message=None):
"""Add, update, or remove multiple configuration values."""
for k, v in values.items():
if v is not None:
_update_config(client, k, value=v, global_only=global_only)
else:
_update_config(client, k, remove=True, global_only=global_only)


def update_multiple_config():
"""Command for updating config."""
return (
Command()
.command(_update_multiple_config)
.require_migration()
.with_commit(commit_if_empty=False, commit_only=CONFIG_LOCAL_PATH)
)


def _update_config(client, key, *, value=None, remove=False, global_only=False, commit_message=None):
"""Add, update, or remove configuration values."""
section, section_key = _split_section_and_key(key)
if remove:
Expand All @@ -43,14 +60,28 @@ def update_config(client, key, *, value=None, remove=False, global_only=False, c
return value


@pass_local_client
def read_config(client, key, local_only, global_only):
def update_config():
"""Command for updating config."""
return (
Command()
.command(_update_config)
.require_migration()
.with_commit(commit_if_empty=False, commit_only=CONFIG_LOCAL_PATH)
)


def _read_config(client, key, config_filter=ConfigFilter.ALL, as_string=True):
"""Read configuration."""
if key:
section, section_key = _split_section_and_key(key)
value = client.get_value(section, section_key, local_only=local_only, global_only=global_only)
value = client.get_value(section, section_key, config_filter=config_filter)
if value is None:
raise errors.ParameterError('Key "{}" not found.'.format(key))
return value

return client.get_config(local_only=local_only, global_only=global_only)
return client.get_config(config_filter=config_filter, as_string=as_string)


def read_config():
"""Command for updating config."""
return Command().command(_read_config)
73 changes: 49 additions & 24 deletions renku/core/management/config.py
Expand Up @@ -18,12 +18,14 @@
"""Client for handling a configuration."""
import configparser
import os
from enum import Enum
from io import StringIO
from pathlib import Path

import attr
import click
import filelock
from pkg_resources import resource_filename

APP_NAME = "Renku"
"""Application name for storing configuration."""
Expand All @@ -37,6 +39,15 @@ def _get_global_config_dir():
return click.get_app_dir(APP_NAME, force_posix=True)


class ConfigFilter(Enum):
"""Enum of filters over which config files to load. Note: Defaulta always get applied."""

ALL = 1
LOCAL_ONLY = 2
GLOBAL_ONLY = 3
DEFAULT_ONLY = 4


@attr.s
class ConfigManagerMixin:
"""Client for handling global configuration."""
Expand Down Expand Up @@ -77,21 +88,32 @@ def global_config_lock(self):

return filelock.FileLock(lock_file, timeout=0)

def load_config(self, local_only, global_only):
def load_config(self, config_filter=ConfigFilter.ALL):
"""Loads local, global or both configuration object."""
config = configparser.ConfigParser()
if local_only:
config_files = [self.local_config_path]
elif global_only:
config_files = [self.global_config_path]
else:
config_files = [self.global_config_path, self.local_config_path]
config_files = [resource_filename("renku", "data/defaults.ini")]

if config_filter == ConfigFilter.LOCAL_ONLY:
config_files += [self.local_config_path]
elif config_filter == ConfigFilter.GLOBAL_ONLY:
config_files += [self.global_config_path]
elif config_filter == ConfigFilter.ALL:
config_files += [self.global_config_path, self.local_config_path]

if not local_only:
if config_filter != ConfigFilter.LOCAL_ONLY:
with self.global_config_lock:
config.read(config_files)
else:
config.read(config_files)

# transform config section for backwards compatibility
for s in config.sections():
if not s.startswith('renku "'):
continue

config[s[7:-1]] = dict(config.items(s))
config.pop(s)

return config

def store_config(self, config, global_only):
Expand All @@ -112,28 +134,30 @@ def store_config(self, config, global_only):
with open(filepath, "w+") as file:
config.write(file)

return self.load_config(local_only=True, global_only=True)

def get_config(self, local_only=False, global_only=False):
def get_config(self, config_filter=ConfigFilter.ALL, as_string=True):
"""Read all configurations."""
config = self.load_config(local_only=local_only, global_only=global_only)
with StringIO() as output:
config.write(output)
return output.getvalue()
config = self.load_config(config_filter=config_filter)
if as_string:
with StringIO() as output:
config.write(output)
return output.getvalue()
else:
return {f"{s}.{k}": v for s in config.sections() for k, v in config.items(s)}

def get_value(self, section, key, local_only=False, global_only=False):
def get_value(self, section, key, config_filter=ConfigFilter.ALL):
"""Get value from specified section and key."""
config = self.load_config(local_only=local_only, global_only=global_only)
config = self.load_config(config_filter=config_filter)
return config.get(section, key, fallback=None)

def set_value(self, section, key, value, global_only=False):
"""Set value to specified section and key."""
local_only = not global_only
config_filter = ConfigFilter.GLOBAL_ONLY

if local_only:
if not global_only:
config_filter = ConfigFilter.LOCAL_ONLY
self._check_config_is_not_readonly(section, key)

config = self.load_config(local_only=local_only, global_only=global_only)
config = self.load_config(config_filter=config_filter)
if section in config:
config[section][key] = value
else:
Expand All @@ -143,12 +167,13 @@ def set_value(self, section, key, value, global_only=False):

def remove_value(self, section, key, global_only=False):
"""Remove key from specified section."""
local_only = not global_only
config_filter = ConfigFilter.GLOBAL_ONLY

if local_only:
if not global_only:
config_filter = ConfigFilter.LOCAL_ONLY
self._check_config_is_not_readonly(section, key)

config = self.load_config(local_only=local_only, global_only=global_only)
config = self.load_config(config_filter=config_filter)
if section in config:
value = config[section].pop(key, None)

Expand All @@ -163,7 +188,7 @@ def _check_config_is_not_readonly(self, section, key):

readonly_configs = {"renku": [self.DATA_DIR_CONFIG_KEY]}

value = self.get_value(section, key, local_only=True)
value = self.get_value(section, key, config_filter=ConfigFilter.LOCAL_ONLY)
if not value:
return

Expand Down
4 changes: 2 additions & 2 deletions renku/core/management/repository.py
Expand Up @@ -34,7 +34,7 @@

from renku.core import errors
from renku.core.compat import Path
from renku.core.management.config import RENKU_HOME
from renku.core.management.config import RENKU_HOME, ConfigFilter
from renku.core.models.projects import Project
from renku.core.models.provenance.activity import ActivityCollection
from renku.core.models.provenance.provenance_graph import ProvenanceGraph
Expand Down Expand Up @@ -144,7 +144,7 @@ def __attrs_post_init__(self):
path.relative_to(path)
self.renku_path = path

data_dir = self.get_value("renku", self.DATA_DIR_CONFIG_KEY, local_only=True)
data_dir = self.get_value("renku", self.DATA_DIR_CONFIG_KEY, config_filter=ConfigFilter.LOCAL_ONLY)
self.data_dir = data_dir or self.data_dir

self._subclients = {}
Expand Down
2 changes: 1 addition & 1 deletion renku/core/management/storage.py
Expand Up @@ -126,7 +126,7 @@ def renku_lfs_ignore(self):
@property
def minimum_lfs_file_size(self):
"""The minimum size of a file in bytes to be added to lfs."""
size = self.get_value("renku", "lfs_threshold") or "100kb"
size = self.get_value("renku", "lfs_threshold")

return parse_file_size(size)

Expand Down
6 changes: 6 additions & 0 deletions renku/data/defaults.ini
@@ -0,0 +1,6 @@
[interactive]
default_url = /lab

[renku]
autocommit_lfs = false
lfs_threshold = 100kb

0 comments on commit 07973c6

Please sign in to comment.