import json import click from pathlib import Path import logging from logging.handlers import RotatingFileHandler import sys from iottb.contexts import IottbConfig APP_NAME = 'iottb' DB_NAME = 'iottb.db' CFG_FILE_PATH = str(Path(click.get_app_dir(APP_NAME)).joinpath('iottb.cfg')) CONSOLE_LOG_FORMATS = { 0: '%(levelname)s - %(message)s', 1: '%(levelname)s - %(module)s - %(message)s', 2: '%(levelname)s - %(asctime)s - %(module)s - %(funcName)s - %(lineno)d - %(message)s' } LOGFILE_LOG_FORMAT = { 0: '%(levelname)s - %(asctime)s - %(module)s - %(message)s', 1: '%(levelname)s - %(asctime)s - %(module)s - %(funcName)s - %(message)s', 2: '%(levelname)s - %(asctime)s - %(module)s - %(funcName)s - %(lineno)d - %(message)s' } MAX_VERBOSITY = len(CONSOLE_LOG_FORMATS) - 1 assert len(LOGFILE_LOG_FORMAT) == len(CONSOLE_LOG_FORMATS), 'Log formats must be same size' logger = logging.getLogger(__name__) def setup_logging(verbosity, debug): """ Setup root logger for iottb """ log_level = logging.ERROR handlers = [] date_format = '%Y-%m-%d %H:%M:%S' if verbosity > 0: log_level = logging.WARNING if verbosity > MAX_VERBOSITY: verbosity = MAX_VERBOSITY log_level = logging.INFO assert verbosity <= MAX_VERBOSITY, f'Verbosity must be <= {MAX_VERBOSITY}' console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(logging.Formatter(CONSOLE_LOG_FORMATS[verbosity], datefmt=date_format)) console_handler.setLevel(logging.DEBUG) # can keep at debug since it depends on global level? handlers.append(console_handler) if debug: log_level = logging.DEBUG # Logfile logs INFO+, no debugs though file_handler = RotatingFileHandler(f'{APP_NAME}.log', maxBytes=10240, backupCount=5) file_handler.setFormatter(logging.Formatter(LOGFILE_LOG_FORMAT[verbosity], datefmt=date_format)) file_handler.setLevel(logging.INFO) # finnish root logger setup handlers.append(file_handler) # Force this config to be applied to root logger logging.basicConfig(level=log_level, handlers=handlers, force=True) def create_default_config(cfg_file): """Create default iottb config file.""" logger.info(f'Creating default config file at {cfg_file}') defaults = { 'DefaultDatabase': DB_NAME, 'DefaultDatabasePath': str(Path.home()) } try: cfg_file.parent.mkdir(exist_ok=True) with cfg_file.open('w') as config_file: defaults_dict = json.dumps(defaults, indent=4) config_file.write(defaults_dict) except IOError as e: logger.error(f"Failed to create default configuration file at {cfg_file}: {e}") raise RuntimeError(f"Failed to create configuration file: {e}") from e return defaults def load_config(cfg_file): """Loads or creates default configuration from given file path.""" logger.info("Loading iottb config file") if not cfg_file.is_file(): return create_default_config(cfg_file) with open(cfg_file, 'r') as config_file: return json.load(config_file) @click.group() @click.option('-v', '--verbosity', count=True, type=click.IntRange(0, 3), default=0, help='Set verbosity') @click.option('-d', '--debug', is_flag=True, default=False, help='Enable debug mode') @click.option('--cfg-file', type=click.Path(), default=Path(click.get_app_dir(APP_NAME)).joinpath('iottb.cfg'), envvar='IOTTB_CONF_HOME', help='Path to iottb config file') @click.pass_context def cli(ctx, verbosity, debug, cfg_file): ctx.ensure_object(dict) # Make sure context is ready for use logger.info("Starting execution.") ctx.obj['CONFIG'] = IottbConfig(cfg_file) # Load configuration directly setup_logging(verbosity, debug) # Setup logging based on the loaded configuration and other options @click.command() @click.option('-d', '--dest', default=str(Path.home()), type=str, help='Location to put (new) iottb database') @click.option('--name', default=DB_NAME, type=str, help='Name of new database.') @click.option('--update-default/ --no-update-default', default=True, help='If new db should be set as the new default') @click.pass_context def init_db(ctx, dest, name, update_default): logger.debug('TOP init_db') @click.command @click.option('--obj', '--object', type=click.Choice(['cfg', 'db', 'dev', 'cap']), help='Type of file to edit.') @click.option('--file', type=click.Choice(['iottb.cfg', ''])) def edit(ctx, file, table, key, value): """Edit config or metadata files. TODO: Implement""" pass @click.command @click.confirmation_option(prompt="Are you certain that you want to delete the cfg file?") def rm_cfg(): """ Removes the cfg file from the filesystem. This is mostly a utility during development. Once non-standard database locations are implemented, deleting this would lead to iottb not being able to find them anymore. """ Path(CFG_FILE_PATH).unlink() click.echo(f'Iottb configuration removed at {CFG_FILE_PATH}') ################################################################################## # Add all subcommands to group here ################################################################################# cli.add_command(init_db) cli.add_command(rm_cfg) cli.add_command(edit) if __name__ == '__main__': cli(auto_envvar_prefix='IOTTB')