import argparse
import configparser
import os
import sys
from timeit import default_timer as timer
from sqlalchemy import create_engine, exc
from sqlalchemy.orm import sessionmaker
import quarg.actions as actions
import quarg.database.filters as filters
import quarg.quassel.formatter as formatter
from quarg.database.tables import Backlog, Buffer, Network, QuasselUser, Sender
from quarg.utils import errx
# pylint: disable=line-too-long
cli = argparse.ArgumentParser()
cli.add_argument('keyword', nargs='*', help='match messages containing this keyword')
cli.add_argument('-d', action='store_true', dest='debug', help='print debug and SQL query information')
cli.add_argument('-e', action='store_true', dest='expr', help='interpret keywords as LIKE expression')
cli.add_argument('-l', dest='limit', metavar='NUM', type=int, help='limit the number of matches')
cli.add_argument('-o', action=actions.ParseOrder, dest='order', metavar='asc|desc', help='sort matches in this order')
matchers = cli.add_argument_group('matching message context')
matchers.add_argument('-b', action='append', dest='buffer', help='match this buffer')
matchers.add_argument('-B', action=actions.ParseBufferType, dest='buftype', help='match buffers of this type')
matchers.add_argument('-n', action='append', dest='nick', help='match this nickname')
matchers.add_argument('-m', action='append', dest='prefix', help='match nicknames with this channel membership prefix')
matchers.add_argument('-N', action='append', dest='network', help='match this network')
matchers.add_argument('-Q', action='append', dest='user', help='match this quassel user')
matchers.add_argument('-t', action=actions.ParseMessageType, dest='msgtype', help='match this message type')
matchers.add_argument('-f', action=actions.ParseMessageFlag, dest='msgflag', help='match this message flag')
date_matchers = cli.add_argument_group('matching message timestamps')
date_matchers.add_argument('--after', action=actions.ParseDate, metavar='DATE', help='sent after this date')
date_matchers.add_argument('--before', action=actions.ParseDate, metavar='DATE', help='sent before this date')
date_matchers.add_argument('--around', action=actions.ParseAround, metavar='DATE[/RANGE]', help='sent RANGE hours/minutes before or after this date')
joined_group = matchers.add_mutually_exclusive_group()
joined_group.add_argument('--joined', default=None, action='store_true', dest='joined', help='match buffers which are currently joined')
joined_group.add_argument('--no-joined', default=None, action='store_false', dest='joined', help='match buffers which are not currently joined')
# pylint: enable=line-too-long
Session = sessionmaker()
def get_config():
xdg_config_home = os.path.expanduser('~/.config/')
if 'XDG_CONFIG_HOME' in os.environ:
xdg_config_home = os.environ['XDG_CONFIG_HOME']
path = os.path.join(xdg_config_home, 'quarg', 'config')
config = configparser.ConfigParser()
config.read(path)
return config
def check_args(args):
if args.around is not None:
if args.before is not None or args.after is not None:
errx('--around cannot be used with --after or --before')
def collect_predicates(args):
funs = {
'keyword': filters.msg_like if args.expr else filters.msg_contains,
'buffer': filters.buffer,
'nick': filters.nick,
'after': filters.time_after,
'before': filters.time_before,
'around': filters.time_around,
'user': filters.user,
'network': filters.network,
'msgflag': filters.msgflag,
'msgtype': filters.msgtype,
'buftype': filters.buftype,
'prefix': filters.prefix,
'joined': filters.joined,
}
for key, value in vars(args).items():
# ignore unset or empty arguments
# Note: 'if not value' does not work here because 'joined' can be falsy
# but still needs to be passed to filters.joined
if value is None or value == []:
continue
# ignore arguments that do not map to predicates
if key not in funs:
continue
if args.debug:
print(f'{key}: {value}', file=sys.stderr)
fun = funs[key]
if isinstance(value, list):
yield filters.any_filter(fun, value)
else:
yield fun(value)
def prepare_query(session, predicates, args):
query = session.query(Backlog).join(Sender).join(Buffer).join(Network).join(QuasselUser)
for predicate in predicates:
query = query.filter(predicate)
order = Backlog.time.asc()
if args.order == 'desc':
order = Backlog.time.desc()
query = query.order_by(order)
if args.limit:
query = query.limit(args.limit)
return query
def time_query(query):
start = timer()
rows = query.all()
end = timer()
return rows, end - start
def main():
args = cli.parse_args()
check_args(args)
config = get_config()
if not config.has_option('Database', 'url'):
errx('No database URL set in config file.')
engine = create_engine(config.get('Database', 'url'), echo=args.debug)
session = Session(bind=engine)
predicates = list(collect_predicates(args))
if not predicates:
errx('Nothing to match.')
try:
query = prepare_query(session, predicates, args)
rows, time = time_query(query)
for row in rows:
print(formatter.format_from(row))
except exc.SQLAlchemyError as err:
errx(err)
except (KeyboardInterrupt, BrokenPipeError):
session.rollback()
session.close()
sys.exit(1)
print(f'Query returned {len(rows)} lines in {time:.4f} seconds.', file=sys.stderr)
if __name__ == "__main__":
main()