alexandria.py
author Dmitriy Morozov <dmitriy@mrzv.org>
Wed, 15 Oct 2008 10:22:29 -0700
changeset 17 eeafcec1814f
parent 16 eccbe49587f2
child 18 928560d49795
permissions -rwxr-xr-x
Don't crash if there are no authors

#!/usr/bin/env python

import os, sys, os.path
import hashlib
import terminal
from optparse import OptionParser
from ConfigParser import SafeConfigParser
from models import Author, Paper, Tag, AuthorNickname, initDatabase, session, asc

db_filename = 'alexandria.db'
term = terminal.TerminalController()
color = {'title':   term.GREEN + term.BOLD, 
         'author':  term.YELLOW + term.BOLD,
         'label':   term.CYAN + term.BOLD,
         'path':    term.NORMAL,
         'hash':    term.RED + term.BOLD,
         'error':   term.RED + term.BOLD,
         'normal':  term.NORMAL}

def find_database(starting_path = None):
    if not starting_path: starting_path = '.'

    # Walk up until "alexandria.db" is found
    # return (True, path) if found, (False, os.path.join(starting_path, 'alexandria.db')) otherwise
    directory = starting_path
    while os.path.abspath(directory) != '/':
        if os.path.exists(os.path.join(directory, db_filename)):
            break
        directory = os.path.abspath(os.path.join(directory, '..'))
    else:
        return (False, os.path.join(starting_path, db_filename))
    return (True, os.path.join(directory, db_filename))

def add(args, options):
    """add a paper to the database"""
    path = args[0]
    if not os.path.exists(path):
        print _colorize_string('error', "Path %s does not exist. Cannot add paper." % path)
        return

    m = hashlib.md5()
    fd = open(path, 'r')
    m.update(fd.read())
    fd.close()

    path = _short_path(path, options)
    p = Paper.get_by(path = path) or Paper.get_by(md5 = m.hexdigest())
    if p is not None:
        print _colorize_string('error', "Paper already exists, use update")
        print '--------------------------------'
        _show_paper(p)
        return

    p = Paper(path = path, md5 = m.hexdigest())
    _set_options(p, options, required = ['title'])

    session.commit()
    _show_paper(p)

def update(args, options):
    """update paper by hash"""
    p = Paper.query.filter(Paper.md5.startswith(args[0])).one()
    if len(args) > 1: p.path = _short_path(args[1], options)
    _set_options(p, options)
    session.commit()
    _show_paper(p)

def view(args, options):
    """view paper by hash"""
    if len(args) < 1: return
    p = Paper.query.filter(Paper.md5.startswith(args[0])).all()
    if not p: 
        print _colorize_string('error', 'No such paper')
        return
    if len(p) > 1:
        print _colorize_string('error', 'Too many choices')
        return
    else:
        p = p[0]
    if len(args) > 1:   viewer = args[1]
    else:               viewer = default_viewer
    os.system('%s %s' % (viewer, os.path.join(options.commonpath, p.path.strip('/'))))

def remove(args, options):
    """remove paper by hash"""
    if len(args) < 1: return
    p = Paper.query.filter(Paper.md5.startswith(args[0])).one()
    if not p: return
    print "Removing"
    _show_paper(p)
    p.delete()
    session.commit()

def list(args, options):
    """list papers in the database"""
    papers = Paper.query.order_by(asc(Paper.title))

    # Refactor with _set_options()
    for label_with_commas in (options.labels or []):
        labels = label_with_commas.split(',')
        for label in labels:
            label = unicode(label.strip())
            label = label.replace('*', '%')             # allow for glob style-pattern
            papers = papers.filter(Paper.tags.any(Tag.name.like(label)))

    for author_with_commas in (options.authors or []):
        authors = author_with_commas.split(',')
        for author in authors:
            author = unicode(author.strip())
            an = AuthorNickname.get_by(name = author)
            if an: a = an.author.name
            else:  a = author.replace('*', '%')
            papers = papers.filter(Paper.authors.any(Author.name.like(a)))

    print
    for p in papers.all():
        _show_paper(p)
        print

def count(args, options):
    """returns the count of papers matching given criteria in the database"""
    papers = Paper.query

    # Refactor with _set_options()
    for label_with_commas in (options.labels or []):
        labels = label_with_commas.split(',')
        for label in labels:
            label = unicode(label.strip())
            label = label.replace('*', '%')             # allow for glob style-pattern
            papers = papers.filter(Paper.tags.any(Tag.name.like(label)))

    for author_with_commas in (options.authors or []):
        authors = author_with_commas.split(',')
        for author in authors:
            author = unicode(author.strip())
            an = AuthorNickname.get_by(name = author)
            if an: a = an.author.name
            else:  a = author.replace('*', '%')
            papers = papers.filter(Paper.authors.any(Author.name.like(a)))

    print "Count:", papers.count()

def authors(args, options):
    """List authors, add, and remove aliases
  ax authors                 - list authors
  ax authors NAME ALIAS      - add ALIAS to AUTHOR's aliases
  ax authors ~ALIAS          - remove ALIAS"""
    
    if len(args) > 0:
        if args[0][0] == '~':
            an = AuthorNickname.get_by(name = unicode(args[0][1:]))
            if an:
                an.delete()
                session.commit()
        else:
            a =  Author.get_by_or_init(name = unicode(args[0]))
            an = AuthorNickname.get_by_or_init(name = unicode(args[1]))
            an.author = a
            session.commit()

    _show_authors(options)

def labels(args, options):
    """rename and/or list labels"""
    if len(args) >= 2:
        t = Tag.get_by(name = unicode(args[0]))
        t2 = Tag.get_by(name = unicode(args[1]))
        if t and not t2:
            t.name = unicode(args[1])
        elif t and t2:
            for p in t.papers:
                t2.papers.append(p)
                t.papers.remove(p)
            t.delete()
    session.commit()

    print "Labels:"
    for t in Tag.query.order_by(asc(Tag.name)).all():
        if len(t.papers) == 0:                  # clean the database
            t.delete()
            continue
        print '  (%d) %s' % (len(t.papers), t.name)
    session.commit()

def info(args, options):
    """Show information about a path"""

    if len(args) == 0:
        print _colorize_string('error', 'Need path to check as an argument.')

    path = _short_path(args[0], options)
    paper = Paper.get_by(path = path)

    if paper:
        _show_paper(paper)
    else:
        print "No such path %s found" % _colorize_string('path', path)

def _short_path(path, options):
    path = os.path.abspath(path)
    commonpath = os.path.commonprefix([options.commonpath, path])
    path = path[len(commonpath):]
    path = path.strip('/')
    return path

def _set_options(p, options, required = []):
    title = options.title or ('title' in required) and raw_input("Enter title: ")
    if title:
        p.title = unicode(title)

    for label_with_commas in (options.labels or []):
        labels = label_with_commas.split(',')
        for label in labels:
            label = unicode(label.strip())
            if label[0] == '-':         # remove label
                t = Tag.get_by(name = label[1:])
                t.papers.remove(p)
            else:                       # add label
                t = Tag.get_by_or_init(name = label)
                t.papers.append(p)

    for author_with_commas in (options.authors or []):
        authors = author_with_commas.split(',')
        for author in authors:
            author = unicode(author.strip())
            if author[0] == '-':
                author = author[1:]
                remove_author = True
            else:
                remove_author = False
            an = AuthorNickname.get_by(name = author)
            if an: a = an.author
            else:  a = Author.get_by_or_init(name = author)
            if remove_author:
                a.papers.remove(p)
            else:
                a.papers.append(p)

def _show_authors(options):
    print "Authors:"
    authors = Author.query.all()
    _sort_authors(authors, lambda x: x.name)
    for a in authors:
        if (len(a.papers) > 0 and options.all) or len(a.nicknames) > 0:
            print '  %s (%d):' % (a.name, len(a.papers)),
            if len(a.nicknames) > 0:
                for an in a.nicknames[:-1]:
                    print an.name + ',',
                #print '%s: %s' % (a.nicknames[-1], a.name)
                print a.nicknames[-1],
            print

def _sort_authors(authors, name = lambda x: x):
    authors.sort(lambda x,y: cmp(name(x).split()[-1], name(y).split()[-1]))

def _show_paper(paper):
    print _colorize_string('title', paper.title)
    authors = [str(a) for a in paper.authors]
    _sort_authors(authors)
    for author in authors[:-1]:
        print '%s,' % _colorize_string('author', author),
    if len(authors): print '%s' % _colorize_string('author', authors[-1])
    print 'Labels:',
    for tag in paper.tags:
        print '+%s' % _colorize_string('label', tag),
    print color['normal']
    print "Path:   %s" % _colorize_string('path', paper.path)
    print "Hash:   %s" % _colorize_string('hash', paper.md5)

def _colorize_string(clr, str):
    return '%s%s%s' % (color[clr], str, color['normal'])

def _find_command(args, commands):
    if len(args) == 0: return None
    candidates = []
    for cmd in commands:
        func = cmd
        if func.__name__.startswith(args[0]):
            candidates += [func]

    if len(candidates) > 1:
        print "Ambiguous choices:",
        for c in candidates: print c.__name__,
        print
    
    if len(candidates) == 1:
        return candidates[0]
    else:
        return None


commands = [add, list, authors, update, view, remove, labels, info, count]


if __name__ == "__main__":
    usage =  '%prog COMMAND OPTIONS\n'
    usage += 'Commands:\n'
    for cmd in commands:
        func, description = cmd, cmd.__doc__.split('\n')[0]
        usage += '  %-10s - %s\n' % (func.__name__, description)
    
    # Parse config
    config = SafeConfigParser()
    config.read([os.path.expanduser('~/.alexandria')])
    dbpath = os.path.expanduser(config.get('paths', 'dbpath'))
    commonpath = os.path.expanduser(config.get('paths', 'common'))
    
    try:
        default_viewer = config.get('setup', 'viewer')
    except:
        default_viewer = 'acroread'

    # Parse options
    parser = OptionParser(usage = usage, add_help_option = False)
    parser.add_option('-a', '--author',     action='append', dest='authors')
    parser.add_option('-t', '--title',      dest='title')
    parser.add_option('-l', '--label',      action='append', dest='labels', help='"label" to add a label or "-label" to remove a label')
    parser.add_option('-D', '--database',   dest='database', help='directory with the database')
    parser.add_option(      '--all',        action='store_true', dest='all', help='show all')
    parser.add_option('-h', '--help',       action='store_true', dest='show_help', help='show help message')
    (options, args) = parser.parse_args()
    
    # Find command
    cmd = _find_command(args, commands)
    
    # Show help if requested
    if options.show_help:
        if cmd:
            print cmd.__doc__
        else:
            parser.print_help()
        sys.exit()

    # Find database
    found, path = find_database(options.database or dbpath)
    initDatabase(path, not found)
    
    # Augment options
    options.dbpath = path
    options.commonpath = commonpath
    
    # Execute command if found
    if cmd: cmd(args[1:], options)