#!/usr/bin/env python
import os, sys, os.path
import hashlib
import terminal
from opster import command, dispatch
from ConfigParser import SafeConfigParser
from models import Author, Paper, Tag, AuthorNickname, initDatabase, session, asc
# suppress the deprecation warning in elixir 0.6.1
import warnings
warnings.simplefilter('ignore', DeprecationWarning, 412)
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 parse_config():
# Parse config
config = SafeConfigParser()
config.read([os.path.expanduser('~/.alexandria')])
cfg = {}
cfg['dbpath'] = os.path.expanduser(config.get('paths', 'dbpath'))
cfg['commonpath'] = os.path.expanduser(config.get('paths', 'common'))
try:
cfg['default_viewer'] = config.get('setup', 'viewer')
except:
cfg['default_viewer'] = 'acroread'
return cfg
def initDB(database):
# Find database
found, path = find_database(database)
initDatabase(path, not found)
laopts = [('l', 'label', [], 'label of the document'),
('a', 'author', [], 'author of the document')]
latopts = laopts + [('t', 'title', '', 'paper title')]
dbopts = [('D', 'database', '', 'directory with the database')]
@command(options = latopts + dbopts,
usage = '%name PATH [-t TITLE] [-l LABEL]* [-a AUTHOR]*')
def add(cfg, path, **opts):
"""Add a paper to the database"""
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 = _short_path(cfg, path)
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, opts, required = ['title'])
session.commit()
_show_paper(p)
@command(options = latopts + dbopts,
usage = '%name HASH [-t TITLE] [-l LABEL]* [-a AUTHOR]*')
def update(cfg, hash, path = None, **opts):
"""Update paper by hash"""
p = _find_paper(hash)
if not p: return
if path: p.path = _short_path(cfg, path)
_set_options(p, opts)
session.commit()
_show_paper(p)
@command(dbopts,
usage = '%name HASH [VIEWER]')
def view(cfg, hash, viewer = None, **opts):
"""View paper by hash"""
p = _find_paper(hash)
if not p: return
viewer = viewer or cfg['default_viewer']
os.system('%s %s' % (viewer, os.path.join(cfg['commonpath'], p.path.strip('/'))))
@command(dbopts,
usage = '%name HASH')
def remove(cfg, hash):
"""Remove paper by hash"""
p = _find_paper(hash)
if not p: return
print "Removing"
_show_paper(p)
p.delete()
session.commit()
@command(options = laopts + dbopts,
usage = '%name [-l LABEL]* [-a AUTHOR]*')
def list(cfg, **opts):
"""List papers in the database"""
papers = Paper.query.order_by(asc(Paper.title))
# TODO: Refactor with _set_options()
for label_with_commas in (opts['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 (opts['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():
_show_paper(p)
print
@command(options = laopts + dbopts,
usage = '%name [-l LABEL]* [-a AUTHOR]*')
def count(cfg, **opts):
"""Prints the count of papers matching given criteria in the database"""
papers = Paper.query
# TODO: Refactor with _set_options()
for label_with_commas in (opts['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 (opts['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()
@command([('a', 'all', False, 'show all')] + dbopts,
usage='%name [-a] or NAME ALIAS or ~ALIAS')
def authors(cfg, name = None, alias = None, **opts):
"""List authors, add, and remove aliases
authors - list authors
authors NAME ALIAS - add ALIAS to AUTHOR's aliases
authors ~ALIAS - remove ALIAS
"""
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()
_show_authors(opts['all'])
@command(dbopts,
usage = '%name [NAME [NEW_NAME]]')
def labels(cfg, name = None, new = None):
"""List and/or rename labels"""
if new:
t = Tag.get_by(name = unicode(name))
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()
@command(dbopts,
usage='%name PATH')
def info(cfg, path):
"""Show information about a path"""
path = _short_path(cfg, path)
paper = Paper.get_by(path = path)
if paper:
_show_paper(paper)
else:
print "No such path %s found" % _colorize_string('path', path)
@command(dbopts,
usage='%name PATH')
def rehash(cfg, path):
"""Rehash the paper at the given path"""
m = hashlib.md5()
with open(path, 'r') as fd:
m.update(fd.read())
path = _short_path(cfg, path)
paper = Paper.get_by(path = path)
if paper:
paper.md5 = m.hexdigest()
session.commit()
_show_paper(paper)
else:
print "No such path %s found" % _colorize_string('path', path)
def _short_path(cfg, path):
path = os.path.abspath(path)
commonpath = os.path.commonprefix([cfg['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['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(all):
print "Authors:"
authors = Author.query.all()
_sort_authors(authors, lambda x: x.name)
for a in authors:
if (len(a.papers) > 0 and 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_paper(hash):
p = Paper.query.filter(Paper.md5.startswith(hash)).all()
if not p:
print _colorize_string('error', 'No such paper')
return None
if len(p) > 1:
print _colorize_string('error', 'Too many choices')
for pp in p:
print
_show_paper(pp)
return None
else:
return p[0]
# Decorator that parses config and initializes the database
def init(func):
if func.__name__ == 'help_inner':
return func
def inner(*args, **kwargs):
cfg = parse_config()
initDB(kwargs.get('database') or cfg['dbpath'])
if 'database' in kwargs:
del kwargs['database']
func(cfg, *args, **kwargs)
return inner
if __name__ == "__main__":
dispatch(middleware = init)