Add config init flow from a file in a config directory

This commit is contained in:
Tim Van Baak 2020-01-01 18:11:27 -08:00
parent 04db9a020f
commit 7a85604ece
4 changed files with 145 additions and 25 deletions

View File

@ -55,15 +55,34 @@ def get_parser(valid_commands):
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Available commands:\n{}\n".format(command_descs), description="Available commands:\n{}\n".format(command_descs),
formatter_class=argparse.RawDescriptionHelpFormatter) formatter_class=argparse.RawDescriptionHelpFormatter)
# The config directory.
parser.add_argument("--config-dir",
dest="config_dir",
default=os.environ.get(configs.ENV_CONFIG_DIR, "./config"),
help="The config directory for Amanuensis")
# Logging settings.
parser.add_argument("--verbose", "-v",
action="store_true",
dest="verbose",
help="Enable verbose console logging")
parser.add_argument("--log-file",
dest="log_file",
default=os.environ.get(configs.ENV_LOG_FILE),
help="Enable verbose file logging")
parser.add_argument("--log-file-size",
dest="log_file_size",
default=os.environ.get(configs.ENV_LOG_FILE_SIZE),
help="Maximum rolling log file size")
parser.add_argument("--log-file-num",
dest="log_file_num",
default=os.environ.get(configs.ENV_LOG_FILE_NUM),
help="Maximum rolling file count")
# Lexicon settings.
parser.add_argument("-n", parser.add_argument("-n",
metavar="LEXICON", metavar="LEXICON",
dest="lexicon", dest="lexicon",
help="The name of the lexicon to operate on") help="The name of the lexicon to operate on")
parser.add_argument("-v", parser.set_defaults(func=lambda args: repl(args) if args.lexicon else parser.print_help())
action="store_true",
dest="verbose",
help="Enable debug logging")
parser.set_defaults(func=repl)
subp = parser.add_subparsers( subp = parser.add_subparsers(
metavar="COMMAND", metavar="COMMAND",
help="The command to execute") help="The command to execute")
@ -90,9 +109,8 @@ def main(argv):
args = get_parser(commands).parse_args(argv) args = get_parser(commands).parse_args(argv)
# Configure logging. # With the arguments parsed, initialize the configs.
if args.verbose: configs.init(args)
configs.log_verbose()
# Execute command. # Execute command.
args.func(args) args.func(args)

View File

@ -2,8 +2,24 @@
from argparse import ArgumentParser as AP from argparse import ArgumentParser as AP
from functools import wraps from functools import wraps
#
# The cli module must not import other parts of the application at the module
# level. This is because most other modules depend on the config module. The
# config module may depend on __main__'s commandline parsing to locate config
# files, and __main__'s commandline parsing requires importing (but not
# executing) the functions in the cli module. Thus, cli functions must only
# import the config module inside the various command methods, which are only
# run after commandline parsing has already occurred.
#
#
# These function wrappers are used to make the command_* methods accept an
# ArgumentParser as a parameter, which it then configures with the given
# argument and returns. This way, we can configure each command's subparser
# in this module without having to write a separate function to configure it.
#
def add_argument(*args, **kwargs): def add_argument(*args, **kwargs):
"""Passes the given args and kwargs to subparser.add_argument"""
def argument_adder(command): def argument_adder(command):
@wraps(command) @wraps(command)
def augmented_command(cmd_args): def augmented_command(cmd_args):
@ -15,6 +31,7 @@ def add_argument(*args, **kwargs):
return argument_adder return argument_adder
def no_argument(command): def no_argument(command):
"""Noops for subparsers"""
@wraps(command) @wraps(command)
def augmented_command(cmd_args): def augmented_command(cmd_args):
if type(cmd_args) is not AP: if type(cmd_args) is not AP:
@ -22,7 +39,9 @@ def no_argument(command):
return augmented_command return augmented_command
@add_argument("--foo", action="store_true") @add_argument("--foo", action="store_true")
def command_a(args): def command_dump(args):
"""a docstring""" """Dumps the global config or the config for the given lexicon"""
print(args.foo) import json
import configs
print(json.dumps(configs.GLOBAL_CONFIG, indent=2))

View File

@ -1,19 +1,93 @@
# Standard library imports
from collections import OrderedDict as odict
import copy
import json
import logging import logging
import logging.config
import os
logger = logging.getLogger("amanuensis") # Module imports
handler = logging.StreamHandler() from errors import MissingConfigError, MalformedConfigError
logger.addHandler(handler)
def log_normal():
logger.setLevel(logging.INFO)
handler.setLevel(logging.INFO)
formatter = logging.Formatter('[{levelname}] {message}', style="{")
handler.setFormatter(formatter)
def log_verbose(): # Environment variable name constants
logger.setLevel(logging.DEBUG) ENV_CONFIG_DIR = "AMANUENSIS_CONFIG_DIR"
handler.setLevel(logging.DEBUG) ENV_LOG_FILE = "AMANUENSIS_LOG_FILE"
formatter = logging.Formatter('[{asctime}] [{levelname}:{filename}:{lineno}] {message}', style="{") ENV_LOG_FILE_SIZE = "AMANUENSIS_LOG_FILE_SIZE"
handler.setFormatter(formatter) ENV_LOG_FILE_NUM = "AMANUENSIS_LOG_FILE_NUM"
# Functions to be used for moving configs on and off of disk.
def read(path):
with open(path, 'r') as config_file:
return json.load(config_file, object_pairs_hook=odict)
def write(config, path):
with open(path, 'w') as dest_file:
json.dump(config, dest_file, allow_nan=False, indent='\t')
#
# The config directory can be set by cli input, so the config infrastructure
# needs to wait for initialization before it can load any configs.
#
CONFIG_DIR = None
GLOBAL_CONFIG = None
def init(args):
"""
Initializes the config infrastructure to read configs from the
directory given by args.config_dir. Initializes logging.
"""
# Check that config dir exists
if not os.path.isdir(args.config_dir):
raise MissingConfigError("Config directory not found: {}".format(args.config_dir))
# Check that global config file exists
global_config_path = os.path.join(args.config_dir, "config.json")
if not os.path.isfile(global_config_path):
raise MissingConfigError("Config directory missing global config file: {}".format(args.config_dir))
# Check that global config file has logging settings
global_config_file = read(global_config_path)
if 'logging' not in global_config_file.keys():
raise MalformedConfigError("No 'logging' section in global config")
# Check that the global config file has a lexicon data directory
if 'lexicon_data' not in global_config_file.keys():
raise MalformedConfigError("No 'lexicon_data' setting in global config")
# Configs verified, use them for initialization
global CONFIG_DIR, GLOBAL_CONFIG
CONFIG_DIR = args.config_dir
GLOBAL_CONFIG = global_config_file
# Initialize logging
init_logging(args)
def init_logging(args):
"""
Initializes logging by using the logging section of the global config
file.
"""
# Get the logging config section
cfg = copy.deepcopy(GLOBAL_CONFIG['logging'])
# Apply any commandline settings to what was defined in the config file
handlers = cfg['loggers']['amanuensis']['handlers']
if args.verbose:
if 'cli-basic' in handlers:
handlers.remove('cli_basic')
handlers.append('cli_verbose')
if args.log_file:
cfg['handlers']['file']['filename'] = args.log_file
handlers.append("file")
# Load the config
try:
logging.config.dictConfig(cfg)
except:
raise MalformedConfigError("Failed to load logging config")
def logger():
"""Returns the main logger"""
return logging.getLogger("amanuensis")
# Global config values, which shouldn't be changing during runtime, are
# accessed through config.get()
def get(key):
"""Gets the given config value from the global config"""
return GLOBAL_CONFIG[key]
log_normal()

9
amanuensis/errors.py Normal file
View File

@ -0,0 +1,9 @@
class AmanuensisError(Exception):
"""Base class for exceptions in amanuensis"""
pass
class MissingConfigError(AmanuensisError):
pass
class MalformedConfigError(AmanuensisError):
pass