alexandria.py
author Dmitriy Morozov <dmitriy@mrzv.org>
Fri, 08 May 2009 17:45:56 -0700
changeset 20 adb636ec4517
parent 19 f929003d4af7
child 21 ecb80f4fd3ec
permissions -rwxr-xr-x
Moved config parsing into constructor to make bash completion work

#!/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))


from    cmdln       import option, Cmdln, CmdlnOptionParser

class Alexandria(Cmdln):
    name = 'alexandria'

    def __init__(self):
        Cmdln.__init__(self)

        # Parse config
        config = SafeConfigParser()
        config.read([os.path.expanduser('~/.alexandria')])
        self.dbpath = os.path.expanduser(config.get('paths', 'dbpath'))
        self.commonpath = os.path.expanduser(config.get('paths', 'common'))
        
        try:
            default_viewer = config.get('setup', 'viewer')
        except:
            default_viewer = 'acroread'

    def get_optparser(self):
        parser = Cmdln.get_optparser(self)
        parser.add_option('-D', '--database',   dest='database', help='directory with the database')
        return parser

    def postoptparse(self):
        # Find database
        found, path = find_database(self.options.database or self.dbpath)
        initDatabase(path, not found)


    @option('-l', '--label',    action='append',    help='label of the document')
    @option('-a', '--author',   action='append',    help='author of the document')
    @option('-t', '--title',                        help='')
    def do_add(self, subcmd, options, path):
        """${cmd_name}: add a paper to the database

           ${cmd_usage}
           ${cmd_option_list}
        """

        if not os.path.exists(path):
            print _colorize_string('error', "Path %s does not exist. Cannot add paper." % path)
            return
        
        m = hashlib.md5()
        with open(path, 'r') as fd:
            m.update(fd.read())
    
        path = self._short_path(path)
        p = Paper.get_by(path = path) or Paper.get_by(md5 = m.hexdigest())
        if p is not None:
            print self._colorize_string('error', "Paper already exists, use update")
            print '--------------------------------'
            self._show_paper(p)
            return
    
        p = Paper(path = path, md5 = m.hexdigest())
        self._set_options(p, options, required = ['title'])
    
        session.commit()
        self._show_paper(p)
    
    @option('-l', '--label',    action='append',    help='label of the document')
    @option('-a', '--author',   action='append',    help='author of the document')
    @option('-t', '--title',                        help='')
    def do_update(self, subcmd, options, hash, path = None):
        """${cmd_name}: update paper by hash

           ${cmd_usage}
           ${cmd_option_list}
        """
        p = Paper.query.filter(Paper.md5.startswith(hash)).one()
        if path: p.path = self._short_path(path)
        self._set_options(p, options)
        session.commit()
        self._show_paper(p)
 
    def do_view(self, subcmd, options, hash, viewer = None):
        """${cmd_name}: view paper by hash

           ${cmd_usage}
           ${cmd_option_list}
        """

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

           ${cmd_usage}
           ${cmd_option_list}
        """
        
        p = Paper.query.filter(Paper.md5.startswith(hash)).one()
        if not p: return
        print "Removing"
        self._show_paper(p)
        p.delete()
        session.commit()
    
    @option('-l', '--label',    action='append',    help='label of the document')
    @option('-a', '--author',   action='append',    help='author of the document')
    def do_list(self, subcmd, options):
        """${cmd_name}: list papers in the database

           ${cmd_usage}
           ${cmd_option_list}
        """
        papers = Paper.query.order_by(asc(Paper.title))
    
        # TODO: Refactor with _set_options()
        for label_with_commas in (options.label 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.author 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():
            self._show_paper(p)
            print
    
    @option('-l', '--label',    action='append',    help='label of the document')
    @option('-a', '--author',   action='append',    help='author of the document')
    def do_count(self, subcmd, options):
        """${cmd_name}: returns the count of papers matching given criteria in the database

           ${cmd_usage}
           ${cmd_option_list}
        """
        papers = Paper.query
    
        # TODO: Refactor with _set_options()
        for label_with_commas in (options.label 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.author 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()
    
    @option('--all',           action='store_true', help='show all')
    def do_authors(self, subcmd, options, name = None, alias = None):
        """List authors, add, and remove aliases

           ${cmd_usage}

               ${name} authors                 - list authors
               ${name} authors NAME ALIAS      - add ALIAS to AUTHOR's aliases
               ${name} authors ~ALIAS          - remove ALIAS
      
           ${cmd_option_list}
      """
        
        if name:
            if name[0] == '~':
                an = AuthorNickname.get_by(name = unicode(name[1:]))
                if an:
                    an.delete()
                    session.commit()
            elif alias:
                a =  Author.get_by_or_init(name = unicode(name))
                an = AuthorNickname.get_by_or_init(name = unicode(alias))
                an.author = a
                session.commit()
    
        self._show_authors(options)
    
    def do_labels(self, subcmd, options, name = None, new = None):
        """${cmd_name}: list and/or rename labels

           ${cmd_usage}
           ${cmd_option_list}
        """
        if new:
            t = Tag.get_by(name = unicode(old))
            t2 = Tag.get_by(name = unicode(new))
            if t and not t2:
                t.name = unicode(new)
            elif t and t2:
                for p in t.papers:
                    t2.papers.append(p)
                    t.papers.remove(p)
                t.delete()
        session.commit()
    
        if not name:
            pattern = u'*'
        else:
            pattern = unicode(name)
        pattern = pattern.replace('*', '%')
    
        print "Labels:"
        for t in Tag.query.filter(Tag.name.like(pattern)).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 do_info(self, subcmd, options, path):
        """${cmd_name}: show information about a path

           ${cmd_usage}
           ${cmd_option_list}
        """
    
        path = self._short_path(path)
        paper = Paper.get_by(path = path)
    
        if paper:
            self._show_paper(paper)
        else:
            print "No such path %s found" % self._colorize_string('path', path)
    
    def _short_path(self, path):
        path = os.path.abspath(path)
        commonpath = os.path.commonprefix([self.commonpath, path])
        path = path[len(commonpath):]
        path = path.strip('/')
        return path
    
    def _set_options(self, 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.label 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.author 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(self, options):
        print "Authors:"
        authors = Author.query.all()
        self._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(self, authors, name = lambda x: x):
        authors.sort(lambda x,y: cmp(name(x).split()[-1], name(y).split()[-1]))
    
    def _show_paper(self, paper):
        print self._colorize_string('title', paper.title)
        authors = [str(a) for a in paper.authors]
        self._sort_authors(authors)
        for author in authors[:-1]:
            print '%s,' % self._colorize_string('author', author),
        if len(authors): print '%s' % self._colorize_string('author', authors[-1])
        print 'Labels:',
        for tag in paper.tags:
            print '+%s' % self._colorize_string('label', tag),
        print color['normal']
        print "Path:   %s" % self._colorize_string('path', paper.path)
        print "Hash:   %s" % self._colorize_string('hash', paper.md5)
    
    def _colorize_string(self, clr, str):
        return '%s%s%s' % (color[clr], str, color['normal'])


if __name__ == "__main__":
    alexandria = Alexandria()
    sys.exit(alexandria.main())