diff --git a/scripts/slack-backup b/scripts/slack-backup index ba16a38..0c2220c 100755 --- a/scripts/slack-backup +++ b/scripts/slack-backup @@ -7,6 +7,7 @@ import argparse import logging from slack_backup import client +from slack_backup import config def setup_logger(args): @@ -47,60 +48,73 @@ def main(): fetch = subparser.add_parser('fetch', help='Update local db with Slack' ' data') - fetch.add_argument('-t', '--token', required=True, help='Slack token - ' + fetch.add_argument('-t', '--token', default=None, help='Slack token - ' 'a string, which can be generated/obtained via ' 'https://api.slack.com/docs/oauth-test-tokens page.') - fetch.add_argument('-u', '--user', default='', help='Username for your ' + fetch.add_argument('-u', '--user', default=None, help='Username for your ' 'Slack account') - fetch.add_argument('-p', '--password', default='', help='Password for your ' - 'Slack account.') - fetch.add_argument('-e', '--team', required=True, help='Team name, which ' + fetch.add_argument('-p', '--password', default=None, help='Password for ' + 'your Slack account.') + fetch.add_argument('-e', '--team', default=None, help='Team name, which ' 'is part of slack url, for example: if url is ' '"https://team.slack.com" than "team" is a name of ' 'the team.') - fetch.add_argument('-v', '--verbose', help='Be verbose. Adding more "v"' - 'will increase verbosity', action="count", default=0) - fetch.add_argument('-q', '--quiet', help='Be quiet. Adding more "q"' - 'will decrease verbosity', action="count", default=0) - fetch.add_argument('-c', '--channels', default=[], nargs='+', + fetch.add_argument('-v', '--verbose', help='Be verbose. Adding more "v" ' + 'will increase verbosity', action="count", + default=None) + fetch.add_argument('-q', '--quiet', help='Be quiet. Adding more "q" will' + ' decrease verbosity', action="count", default=None) + fetch.add_argument('-c', '--channels', default=None, nargs='+', help='List of channels to perform actions on. ' 'Default is all channels.') - fetch.add_argument('-d', '--database', default='', + fetch.add_argument('-d', '--database', default=None, help='Path to the database file.') + fetch.add_argument('-i', '--config', default=None, + help='Use specific config file.') + fetch.set_defaults(func=fetch_data) generate = subparser.add_parser('generate', help='Generate logs out of ' 'data in provided database') - generate.add_argument('-o', '--output', default='logs', help="Output " + generate.add_argument('-o', '--output', default=None, help="Output " "directory for store logs. All logs are organised " "per channel. By default it's `logs' directory") - generate.add_argument('-f', '--format', default='none', + generate.add_argument('-f', '--format', default=None, choices=('text', 'none'), help='Output format. Default is none; only database ' 'is updated by latest messages for all/selected ' 'channels.') - generate.add_argument('-t', '--theme', default='plain', + generate.add_argument('-t', '--theme', default=None, choices=('plain', 'unicode'), help='Choose theme for text output. It doesn\'t ' 'affect other output formats.') - generate.add_argument('-v', '--verbose', help='Be verbose. Adding more "v"' - 'will increase verbosity', action="count", default=0) - generate.add_argument('-q', '--quiet', help='Be quiet. Adding more "q"' - 'will decrease verbosity', action="count", default=0) + generate.add_argument('-v', '--verbose', help='Be verbose. Adding more ' + '"v" will increase verbosity', action="count", + default=None) + generate.add_argument('-q', '--quiet', help='Be quiet. Adding more "q" ' + 'will decrease verbosity', action="count", + default=None) generate.add_argument('-c', '--channels', default=[], nargs='+', help='List of channels to perform actions on. ' 'Default is all channels.') - generate.add_argument('-d', '--database', default='', + generate.add_argument('-d', '--database', default=None, help='Path to the database file.') + generate.add_argument('-i', '--config', default=None, + help='Use specific config file.') + generate.set_defaults(func=generate_raport) args = parser.parse_args() + cfg = config.Config() + msg = cfg.update(args) setup_logger(args) + logging.info(msg) + args.func(args) diff --git a/slack_backup/config.py b/slack_backup/config.py new file mode 100644 index 0000000..dd6e776 --- /dev/null +++ b/slack_backup/config.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Configuration module for slack-backup +""" +import json +import os +import configparser + + +class Config(object): + """Configuration keeper""" + + ints = ['verbose', 'quiet'] + + sections = {'common': ['channels', 'database', 'quiet', 'verbose'], + 'fetch': ['user', 'password', 'team', 'token'], + 'generate': ['output', 'format', 'theme']} + + def __init__(self): + """ + Init. Read config, if exists, and update passed argument parser + object. + """ + + self.cp = configparser.ConfigParser() + self._options = {'channels': [], + 'database': None, + 'quiet': 0, + 'verbose': 0, + 'user': None, + 'password': None, + 'team': None, + 'token': None, + 'output': None, + 'format': None, + 'theme': None} + # This message supposed to be displayed in INFO level. During the time + # of running the code where it should be displayed there is no + # complete information about logging level. Displaying message is + # dependent on the a) config file, b) argument from commandline. Let's + # resolve if user want to have that information or not after merging + # those two sources. If user do not want to see any message in INFO + # level, we shouldn't do so. + self.msg = '' + + def update(self, args): + self.load_config(args) + self.parse_loaded_options() + self.update_args(args) + return self.msg + + def load_config(self, args): + + if not args.config: + path = os.path.join(os.path.abspath('.'), 'slack-backup.ini') + else: + path = args.config + + locations = [path, + './slack-backup.conf', + os.path.expandvars('$XDG_CONFIG_HOME/slack-backup.ini'), + os.path.expandvars('$HOME/.config/slack-backup.ini')] + + for location in locations: + if os.path.exists(location): + self.cp.read(location) + self.msg = 'Found configuration file: %s' % location + break + else: + self.msg = 'No configuration file found' + + def parse_loaded_options(self): + + for section in self.cp.sections(): + if section not in self.sections: + continue + + for option in self.sections[section]: + if option in self.ints: + val = self.cp.getint(section, option, fallback=0) + elif option == 'channels': + val = self.cp.get(section, option, fallback='[]') + val = json.loads(val) + else: + val = self.cp.get(section, option, fallback='') + + self._options[option] = val + + def update_args(self, args): + + # special case, re-set information for verbose/quiet options + if args.verbose is not None and self._options['quiet'] is not None: + self._options['quiet'] = 0 + self._options['verbose'] = args.verbose + if args.quiet is not None and self._options['verbose'] is not None: + self._options['verbose'] = 0 + self._options['quiet'] = args.quiet + + for sec_id in (args.parser, 'common'): + for option in self.sections[sec_id]: + if option in args: + if getattr(args, option) is not None: + continue + + setattr(args, option, self._options[option]) diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..3644f1d --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import argparse +import tempfile +import os +import unittest + +from slack_backup import config + + +CONF = """\ +[common] +channels=["one","two", "three"] +database=dbfname.sqlite +quiet=1 +verbose=2 + +[generate] +output=logs +format=text +theme=plain + +[fetch] +user=someuser@address.com +password=secret +team=myteam +token=xxxx-1111111111-222222222222-333333333333-r4nd0ms7uff +""" + + +class TestConfig(unittest.TestCase): + + def setUp(self): + + fd, self.confname = tempfile.mkstemp() + os.close(fd) + + with open(self.confname, 'w') as fobj: + fobj.write(CONF) + + def tearDown(self): + os.unlink(self.confname) + + def test_config(self): + + self.assertTrue(os.path.exists(self.confname)) + self.assertTrue(os.path.isfile(self.confname)) + + args = argparse.Namespace() + args.config = None + args.parser = 'fetch' + args.verbose = 2 + args.quiet = None + args.channels = None + args.database = None + args.user = None + args.password = None + args.team = None + args.token = None + + conf = config.Config() + conf.update(args) + self.assertDictEqual(vars(args), {'config': None, + 'parser': 'fetch', + 'verbose': 2, + 'quiet': 0, + 'channels': [], + 'database': '', + 'user': None, + 'password': None, + 'team': None, + 'token': None}) + + args = argparse.Namespace() + args.config = self.confname + args.parser = 'fetch' + args.verbose = 2 + args.quiet = None + args.channels = None + args.database = None + args.user = None + args.password = None + args.team = None + args.token = None + + conf = config.Config() + conf.update(args) + + self.assertEqual(conf._options['verbose'], 2) + self.assertListEqual(conf._options['channels'], + ['one', 'two', 'three']) + self.assertEqual(conf._options['database'], 'dbfname.sqlite') + self.assertEqual(conf._options['user'], 'someuser@address.com') + + self.assertDictEqual(vars(args), {'config': self.confname, + 'parser': 'fetch', + 'verbose': 2, + 'quiet': 0, + 'channels': ['one', 'two', 'three'], + 'database': 'dbfname.sqlite', + 'user': 'someuser@address.com', + 'password': 'secret', + 'team': 'myteam', + 'token': 'xxxx-1111111111-' + '222222222222-333333333333-' + 'r4nd0ms7uff'}) + + # override some conf options with commandline + args = argparse.Namespace() + args.config = self.confname + args.parser = 'fetch' + args.verbose = None + args.quiet = 2 + args.channels = ['foo'] + args.database = None + args.user = 'joe' + args.password = 'ultricies' + args.team = '' + args.token = 'the token' + + conf = config.Config() + conf.update(args) + + self.assertDictEqual(vars(args), {'config': self.confname, + 'parser': 'fetch', + 'verbose': 0, + 'quiet': 2, + 'channels': ['foo'], + 'database': 'dbfname.sqlite', + 'user': 'joe', + 'password': 'ultricies', + 'team': '', + 'token': 'the token'})