From 7a85604ece9f53166052f6936602a2b975b4fbb0 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Wed, 1 Jan 2020 18:11:27 -0800 Subject: [PATCH] Add config init flow from a file in a config directory --- amanuensis/__main__.py | 34 ++++++++++---- amanuensis/cli.py | 25 ++++++++-- amanuensis/configs.py | 102 +++++++++++++++++++++++++++++++++++------ amanuensis/errors.py | 9 ++++ 4 files changed, 145 insertions(+), 25 deletions(-) create mode 100644 amanuensis/errors.py diff --git a/amanuensis/__main__.py b/amanuensis/__main__.py index 229f23f..77c9dfb 100644 --- a/amanuensis/__main__.py +++ b/amanuensis/__main__.py @@ -55,15 +55,34 @@ def get_parser(valid_commands): parser = argparse.ArgumentParser( description="Available commands:\n{}\n".format(command_descs), 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", metavar="LEXICON", dest="lexicon", help="The name of the lexicon to operate on") - parser.add_argument("-v", - action="store_true", - dest="verbose", - help="Enable debug logging") - parser.set_defaults(func=repl) + parser.set_defaults(func=lambda args: repl(args) if args.lexicon else parser.print_help()) subp = parser.add_subparsers( metavar="COMMAND", help="The command to execute") @@ -90,9 +109,8 @@ def main(argv): args = get_parser(commands).parse_args(argv) - # Configure logging. - if args.verbose: - configs.log_verbose() + # With the arguments parsed, initialize the configs. + configs.init(args) # Execute command. args.func(args) diff --git a/amanuensis/cli.py b/amanuensis/cli.py index ecce1f6..24d011a 100644 --- a/amanuensis/cli.py +++ b/amanuensis/cli.py @@ -2,8 +2,24 @@ from argparse import ArgumentParser as AP 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): + """Passes the given args and kwargs to subparser.add_argument""" def argument_adder(command): @wraps(command) def augmented_command(cmd_args): @@ -15,6 +31,7 @@ def add_argument(*args, **kwargs): return argument_adder def no_argument(command): + """Noops for subparsers""" @wraps(command) def augmented_command(cmd_args): if type(cmd_args) is not AP: @@ -22,7 +39,9 @@ def no_argument(command): return augmented_command @add_argument("--foo", action="store_true") -def command_a(args): - """a docstring""" - print(args.foo) +def command_dump(args): + """Dumps the global config or the config for the given lexicon""" + import json + import configs + print(json.dumps(configs.GLOBAL_CONFIG, indent=2)) diff --git a/amanuensis/configs.py b/amanuensis/configs.py index f864e2c..d9ebbee 100644 --- a/amanuensis/configs.py +++ b/amanuensis/configs.py @@ -1,19 +1,93 @@ +# Standard library imports +from collections import OrderedDict as odict +import copy +import json import logging +import logging.config +import os -logger = logging.getLogger("amanuensis") -handler = logging.StreamHandler() -logger.addHandler(handler) +# Module imports +from errors import MissingConfigError, MalformedConfigError -def log_normal(): - logger.setLevel(logging.INFO) - handler.setLevel(logging.INFO) - formatter = logging.Formatter('[{levelname}] {message}', style="{") - handler.setFormatter(formatter) -def log_verbose(): - logger.setLevel(logging.DEBUG) - handler.setLevel(logging.DEBUG) - formatter = logging.Formatter('[{asctime}] [{levelname}:{filename}:{lineno}] {message}', style="{") - handler.setFormatter(formatter) +# Environment variable name constants +ENV_CONFIG_DIR = "AMANUENSIS_CONFIG_DIR" +ENV_LOG_FILE = "AMANUENSIS_LOG_FILE" +ENV_LOG_FILE_SIZE = "AMANUENSIS_LOG_FILE_SIZE" +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() diff --git a/amanuensis/errors.py b/amanuensis/errors.py new file mode 100644 index 0000000..4a6de08 --- /dev/null +++ b/amanuensis/errors.py @@ -0,0 +1,9 @@ +class AmanuensisError(Exception): + """Base class for exceptions in amanuensis""" + pass + +class MissingConfigError(AmanuensisError): + pass + +class MalformedConfigError(AmanuensisError): + pass