diff --git a/CouchPotato.py b/CouchPotato.py index b4a6421..20d3bb0 100755 --- a/CouchPotato.py +++ b/CouchPotato.py @@ -33,7 +33,7 @@ class Loader(object): # Get options via arg from couchpotato.runner import getOptions - self.options = getOptions(sys.argv[1:]) + self.options = getOptions(sys.argv[1:], base_path) # Load settings settings = Env.get('settings') diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..f056a0a --- /dev/null +++ b/config.ini @@ -0,0 +1,9 @@ +# Runtime configuration file +# For all available options copy thefile located at +# "/init/runtime-config.ini" to +# "/config.ini" +# and edit it to fit your needs. + + +[default] +daemon: diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 900b1f8..10e0efd 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -13,6 +13,7 @@ import shutil from CodernityDB.database_super_thread_safe import SuperThreadSafeDatabase from argparse import ArgumentParser +import argparse_config from cache import FileSystemCache from couchpotato import KeyHandler, LoginHandler, LogoutHandler from couchpotato.api import NonBlockHandler, ApiHandler @@ -27,9 +28,9 @@ from couchpotato.core.softchroot import SoftChrootInitError try: from tornado.netutil import bind_unix_socket except: pass -def getOptions(args): - +def getOptions(args, base_path): # Options + conf = os.path.join(base_path, "config.ini") parser = ArgumentParser(prog = 'CouchPotato.py') parser.add_argument('--data_dir', dest = 'data_dir', help = 'Absolute or ~/ path of the data dir') @@ -45,7 +46,11 @@ def getOptions(args): dest = 'daemon', help = 'Daemonize the app') parser.add_argument('--pid_file', dest = 'pid_file', help = 'Path to pidfile needed for daemon') - + + # parse applicaions runtime configuration file and set options + if os.path.exists(conf): + argparse_config.read_config_file(parser, conf) + options = parser.parse_args(args) data_dir = os.path.expanduser(options.data_dir if options.data_dir else getDataDir()) diff --git a/init/runtime-config.ini b/init/runtime-config.ini new file mode 100644 index 0000000..def8a5e --- /dev/null +++ b/init/runtime-config.ini @@ -0,0 +1,25 @@ +# A template for the CouchPotatoServer config.ini file + +# Do not change the [default] section heading +[default] + +# Uncomment to run CouchPotato as a daemon +# daemon: + +# Uncomment to specify CouchPotato's data directory +# data_dir: /apps/configs/couchpotato + +# Uncomment to specify the location of CouchPotato's settings file +# config_file: /home/couchpotato/settings.conf + +# Uncomment to specifiy the location of the PID file +# pid_file: /home/couchpotato/cp.pid + +# Uncomment to run CP in debug mode +# debug: + +# Uncomment to log messages to the console instead of the log files +# console_log: + +# Uncomment to limit logging +# quiet: diff --git a/libs/argparse_config.py b/libs/argparse_config.py new file mode 100644 index 0000000..40e2b0f --- /dev/null +++ b/libs/argparse_config.py @@ -0,0 +1,209 @@ +""" +Author: tikitu +""" +import ConfigParser +from argparse import _SubParsersAction, _StoreAction, _StoreConstAction +import argparse + +__version__ = '0.5.1' + + +def get_config_parser(filename): + config_parser = ConfigParser.SafeConfigParser(allow_no_value=True) + config_parser.read([filename]) + return config_parser + + +def read_config_file(arg_parser, filename): + config_parser = get_config_parser(filename) + read_config_parser(arg_parser, config_parser) + + +def read_config_parser(arg_parser, config_parser): + ReadConfig(config_parser=config_parser).walk_parser(arg_parser) + + +def add_config_block_subcommand(arg_parser, subparsers, + config_parser=None, + only_non_defaults=False): + """ + Add a subcommand "config-block" to the arg_parser, to be used as follows: + + In myprog.py: + + subparsers = arg_parser.add_subparsers(..., dest='command') + add_config_block_subcommand(arg_parser, subparsers) + # ... + parsed_args = arg_parser.parse_args() + if parsed_args.command == 'config': + print parsed_args.func(parsed_args) + exit(0) + + On the commandline: + + $ myprog.py config default --username tikitu --secret xyzzy + [default] + username: tikitu + secret: xyzzy + + :param arg_parser: + :param config_parser: + :param dest: + :param only_non_defaults: + :return: None + """ + config_command_parser = subparsers.add_parser('config') + config_command_parser.add_argument('block') + config_command_parser.add_argument('commandline', nargs=argparse.REMAINDER) + + def handle_args(orig_parsed_args): + if config_parser is not None: + read_config_parser(arg_parser, config_parser) + args_for_commandline = list(orig_parsed_args.commandline) + if orig_parsed_args.block == 'default': + subparsers.add_parser('dummy-command') + args_for_commandline.append('dummy-command') + else: + args_for_commandline.insert(0, orig_parsed_args.block) + parsed_args = arg_parser.parse_args(args_for_commandline) + return generate_config(arg_parser, parsed_args, + section=orig_parsed_args.block, + only_non_defaults=only_non_defaults) + + config_command_parser.set_defaults(func=handle_args) + + +def generate_config(arg_parser, parsed_args, section='default', + only_non_defaults=False): + action = GenerateConfig(parsed_args, section, + only_non_defaults=only_non_defaults) + action.walk_parser(arg_parser) + return action.contents + + +class ArgParserWalker(object): + def start_section(self, section_name): + raise NotImplementedError() + + def end_section(self): + raise NotImplementedError() + + def process_parser_action(self, action, is_store_const=False): + raise NotImplementedError() + + def walk_parser(self, arg_parser): + try: + self.start_section('default') + for action in arg_parser._actions: + if isinstance(action, _StoreAction): + self.process_parser_action(action) + elif isinstance(action, _StoreConstAction): + self.process_parser_action(action, is_store_const=True) + elif isinstance(action, _SubParsersAction): + for command, sub_parser in action.choices.items(): + self.start_section(command) + for sub_action in sub_parser._actions: + self.process_parser_action(sub_action) + self.end_section() + self.end_section() + except DefaultError as e: + arg_parser.error( + u'[{section_name}] config option "{option_string}" ' + u'must be {type_transformer}() value, got: {value}'.format( + section_name=e.section_name, + option_string=e.option_string, + type_transformer=e.type_transformer.__name__, + value=e.value + )) + + +class GenerateConfig(ArgParserWalker): + def __init__(self, parsed_args, section, only_non_defaults=False): + self.parsed_args = parsed_args + self._contents = [] + self._only_non_defaults = only_non_defaults + self._section = section + self._in_sections = [] + + def start_section(self, section_name): + self._in_sections.append(section_name) + if section_name == self._section: + if self._contents: + self._contents.append(u'') + self._contents.append(u'[{0}]'.format(section_name)) + + def end_section(self): + self._in_sections.pop() + + @property + def contents(self): + return u'\n'.join(self._contents + [u'']) + + def process_parser_action(self, action, is_store_const=False): + if self._in_sections[-1] != self._section: + return + # take the longest string, likely the most informative + action_name = list(action.option_strings) + action_name.sort(key=lambda s: len(s), reverse=True) + action_name = _convert_option_string(action_name[0]) + + action_value = getattr(self.parsed_args, action.dest, None) + if self._only_non_defaults and action_value == action.default: + action_value = None + if action_value is not None: + if is_store_const: + self._contents.append(action_name) + else: + self._contents.append(u'{action_name}: {default_value}'.format( + action_name=action_name, + default_value=action_value, # hope it prints as wanted... + )) + + +class ReadConfig(ArgParserWalker): + def __init__(self, config_parser=None): + self.sections = [] + self.config_parser = config_parser + + def start_section(self, section_name): + self.sections.append(section_name) + + def end_section(self): + self.sections.pop() + + @property + def current_section(self): + return self.sections[-1] if self.sections else None + + def process_parser_action(self, action, is_store_const=False): + for option_string in action.option_strings: + option_string = _convert_option_string(option_string) + if self.config_parser.has_option(self.current_section, + option_string): + if is_store_const: + action.default = action.const + else: + value = self.config_parser.get(self.current_section, + option_string) + type_transformer = (action.type if action.type is not None + else lambda x: x) + try: + action.default = type_transformer(value) + except: + raise DefaultError(self.current_section, + option_string, + value, + type_transformer) + action.required = False + + +class DefaultError(Exception): + def __init__(self, section_name, option_string, value, type_transformer): + self.section_name = section_name + self.option_string = option_string + self.value = value + self.type_transformer = type_transformer + + +def _convert_option_string(op_s): + return op_s.lstrip('-').replace('-', '_')