Skip to content

Commit

Permalink
Interactive update (#6)
Browse files Browse the repository at this point in the history
* Additional configuration files
* masked_configuration as dict
* Update demo
* Update README
  • Loading branch information
kmagusiak authored Oct 7, 2022
1 parent 5296825 commit d3f585a
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 46 deletions.
41 changes: 25 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,14 @@ To run an application, you need...
version='0.1',
)

## Secrets

When showing the configuration, by default configuration keys which are
secrets, keys or passwords will be masked.
Another good practice is to have a file containing the password which
you can retrieve using `alphaconf.get('secret_file', 'read_strip')`.

## Invoke integration

Just add the lines below to parameterize invoke.
Note that the argument parsing to overwrite configuration will work only
when the script is directly called.
During an interactive session, you can set the application in the current
context.

ns = Collection() # define the invoke configuration
import alphaconf.invoke
alphaconf.setup_configuration({'backup': 'all'})
alphaconf.invoke.run(__name__, ns)
# import other modules
import alphaconf
app = alphaconf.Application()
app.setup_configuration(configuration_paths=[]) # load other files
alphaconf.set_application(app)

## How the configuration is loaded

Expand Down Expand Up @@ -98,3 +89,21 @@ An argument `--select key=template` is a shortcut for
So, `logging: ${oc.select:base.logging.default}` resolves to the configuration
dict defined in base.logging.default and you can select it using
`--select logging=default`.

## Secrets

When showing the configuration, by default configuration keys which are
secrets, keys or passwords will be masked.
Another good practice is to have a file containing the password which
you can retrieve using `alphaconf.get('secret_file', 'read_strip')`.

## Invoke integration

Just add the lines below to parameterize invoke.
Note that the argument parsing to overwrite configuration will work only
when the script is directly called.

ns = Collection() # define the invoke configuration
import alphaconf.invoke
alphaconf.setup_configuration({'backup': 'all'})
alphaconf.invoke.run(__name__, ns)
76 changes: 48 additions & 28 deletions alphaconf/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def configuration(self) -> DictConfig:
assert self.__config is not None
return self.__config

def _get_possible_configuration_paths(self) -> Iterable[str]:
def _get_possible_configuration_paths(self, additional_paths=[]) -> Iterable[str]:
"""List of paths where to find configuration files"""
name = self.name
is_windows = sys.platform.startswith('win')
Expand All @@ -108,6 +108,7 @@ def _get_possible_configuration_paths(self) -> Iterable[str]:
if path and '$' not in path:
for ext in load_file.SUPPORTED_EXTENSIONS:
yield path.format(name + '.' + ext)
yield from additional_paths

def _load_dotenv(self, load_dotenv: Optional[bool] = None):
"""Load dotenv variables (optionally)"""
Expand Down Expand Up @@ -146,6 +147,7 @@ def __load_environ(self, prefixes: Iterable[str]) -> DictConfig:

def _get_configurations(
self,
configuration_paths: List[str] = [],
env_prefixes: Union[bool, Iterable[str]] = True,
) -> Iterable[DictConfig]:
"""List of all configurations that can be loaded automatically
Expand All @@ -165,14 +167,15 @@ def _get_configurations(
yield default_configuration
yield self._app_configuration()
# Read files
for path in self._get_possible_configuration_paths():
if os.path.isfile(path):
_log.debug('Load configuration from %s', path)
conf = load_file.read_configuration_file(path)
if isinstance(conf, DictConfig):
yield conf
else:
yield from conf
for path in self._get_possible_configuration_paths(configuration_paths):
if not (path in configuration_paths or os.path.isfile(path)):
continue
_log.debug('Load configuration from %s', path)
conf = load_file.read_configuration_file(path)
if isinstance(conf, DictConfig):
yield conf
else:
yield from conf
# Environment
prefixes: Optional[Tuple[str, ...]]
if env_prefixes is True:
Expand All @@ -197,6 +200,7 @@ def setup_configuration(
*,
load_dotenv: Optional[bool] = None,
env_prefixes: Union[bool, Iterable[str]] = True,
configuration_paths: List[str] = [],
resolve_configuration: bool = True,
setup_logging: bool = True,
) -> None:
Expand Down Expand Up @@ -224,7 +228,12 @@ def setup_configuration(

# Load and merge configurations
self._load_dotenv(load_dotenv=load_dotenv)
configurations = list(self._get_configurations(env_prefixes=env_prefixes))
configurations = list(
self._get_configurations(
env_prefixes=env_prefixes,
configuration_paths=configuration_paths,
)
)
if self.parsed:
configurations.extend(self.parsed.configurations())
self.__config = cast(DictConfig, OmegaConf.merge(*configurations))
Expand Down Expand Up @@ -279,36 +288,47 @@ def masked_configuration(
*,
mask_base: bool = True,
mask_secrets: bool = True,
mask_keys: List[str] = [],
) -> DictConfig:
mask_keys: List[str] = ['application.uuid'],
) -> dict:
"""Get the configuration as yaml string
:param mask_base: Whether to mask "base" entry
:param mask_secrets: Whether to mask secret keys
:param mask_keys: Which keys to mask
:return: Configuration copy with masked values
"""
config = self.configuration.copy()
from . import SECRET_MASKS

config = cast(dict, OmegaConf.to_container(self.configuration))
if mask_secrets:
config = Application.__mask_secrets(config)
if mask_base:
config['base'] = {key: list(choices.keys()) for key, choices in config.base.items()}
if mask_keys:
config = OmegaConf.masked_copy(
config, [k for k in config.keys() if k not in mask_keys and isinstance(k, str)]
config = Application.__mask_config(
config, lambda p: any(mask(p) for mask in SECRET_MASKS), lambda _: '*****'
)
if mask_base and 'base' not in mask_keys:
config['base'] = Application.__mask_config(
config['base'],
lambda p: isinstance(OmegaConf.select(self.configuration, p), DictConfig),
lambda v: list(v) if isinstance(v, dict) else v,
)
if mask_keys:
config = Application.__mask_config(config, lambda p: p in mask_keys, lambda _: None)
return config

@staticmethod
def __mask_secrets(configuration):
from . import SECRET_MASKS

for key in list(configuration):
if isinstance(key, str) and any(mask(key) for mask in SECRET_MASKS):
configuration[key] = '*****'
elif isinstance(configuration[key], (Dict, DictConfig, dict)):
configuration[key] = Application.__mask_secrets(configuration[key])
return configuration
def __mask_config(config, check, replacement, path=''):
for key in list(config):
value = config[key]
if isinstance(value, (Dict, DictConfig, dict)):
config[key] = value = Application.__mask_config(
value, check, replacement, path + key + '.'
)
if check(path + key):
value = replacement(value)
if value is None:
del config[key]
else:
config[key] = value
return config

def print_help(self, *, usage=None, description=None, arguments=True):
"""Print the help message
Expand Down
7 changes: 5 additions & 2 deletions demo.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"\n",
"positional arguments:\n",
" key=value Configuration items\n",
" server Arguments for the demo\n"
" server Arguments for the demo\n",
" show The name of the selection to show\n",
" exception If set, raise an exception\n"
]
}
],
Expand Down Expand Up @@ -81,10 +83,11 @@
" url: http://default\n",
" user: ${oc.env:USER}\n",
" home: '~'\n",
"show: false\n",
"exception: false\n",
"application:\n",
" name: example\n",
" version: '0.1'\n",
" uuid: 375cb61e-99e3-4865-be7f-892945888869\n",
"example: config\n",
"arg: name\n",
"\n"
Expand Down

0 comments on commit d3f585a

Please sign in to comment.