558 lines
20 KiB
Python
Executable File
558 lines
20 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import sys
|
|
import getopt
|
|
import pgdb
|
|
from fileformat import read_actionlist, write_actionlist
|
|
from google_interface import google_suggest_book_data
|
|
from util import *
|
|
from exc import WorblehatException
|
|
from file_io import tmpfile, tmpfile_name, write, write_stderr, debug, run_editor, encoding_comment
|
|
|
|
# connection = pgdb.connect(database='oysteini_pbb2',
|
|
# user='oysteini_pbb',
|
|
# password='lio5Aide',
|
|
# host='postgres.pvv.ntnu.no');
|
|
|
|
# c = connection.cursor()
|
|
# c.execute('SELECT * from book')
|
|
# print fetchall_dict(c)
|
|
|
|
q_list_books = \
|
|
'SELECT isbn, book.id AS id, title, category, ' \
|
|
'array_to_string(array_agg(person.lastname || \' (\' || person.id || \')\'), \', \') AS persons ' \
|
|
'FROM book ' \
|
|
'LEFT JOIN bookperson ON book.isbn=bookperson.book ' \
|
|
'LEFT JOIN person ON bookperson.person=person.id ' \
|
|
'GROUP BY isbn, book.id, title, category'
|
|
q_list_persons = \
|
|
'SELECT person.id, person.firstname, person.lastname, ' \
|
|
' COUNT(bookperson.id) AS num_books ' \
|
|
'FROM person LEFT JOIN bookperson ON person.id=bookperson.person ' \
|
|
'GROUP BY person.id, person.firstname, person.lastname'
|
|
q_list_categories = \
|
|
'SELECT category.id, category.name, ' \
|
|
' COUNT(book.isbn) AS num_books ' \
|
|
'FROM category ' \
|
|
'LEFT JOIN book ON category.id=book.category ' \
|
|
'GROUP BY category.id, category.name'
|
|
q_persons_for_book = \
|
|
'SELECT person.id, lastname, firstname, relation ' \
|
|
'FROM person ' \
|
|
'INNER JOIN bookperson ON person.id=bookperson.person ' \
|
|
'WHERE bookperson.book=%(isbn)s'
|
|
q_books_for_person = \
|
|
'SELECT isbn, title, relation ' \
|
|
'FROM bookperson ' \
|
|
'INNER JOIN book ON bookperson.book=book.isbn ' \
|
|
'WHERE bookperson.person=%(id)s'
|
|
q_books_for_category = \
|
|
'SELECT isbn, title ' \
|
|
'FROM book ' \
|
|
'WHERE category=%(id)s'
|
|
|
|
q_new_person = \
|
|
'INSERT INTO person (id, lastname, firstname) ' \
|
|
'VALUES (%(id)s, %(lastname)s, %(firstname)s)'
|
|
q_edit_person = \
|
|
'UPDATE person ' \
|
|
'SET lastname=%(lastname)s, firstname=%(firstname)s ' \
|
|
'WHERE id=%(id)s'
|
|
q_delete_person = \
|
|
'DELETE FROM person WHERE id=%(id)s'
|
|
q_new_book = \
|
|
'INSERT INTO book ' \
|
|
' (isbn, id, title, subtitle, category, publisher, ' \
|
|
' published_year, edition, pages, series, description) ' \
|
|
'VALUES (%(isbn)s, %(id)s, %(title)s, %(subtitle)s, %(category)s, %(publisher)s, ' \
|
|
' %(published_year)d, %(edition)s, %(pages)d, %(series)s, %(description)s)'
|
|
q_edit_book = \
|
|
'UPDATE book ' \
|
|
'SET isbn=%(isbn)s, id=%(id)s, title=%(title)s, ' \
|
|
' subtitle=%(subtitle)s, category=%(category)s, ' \
|
|
' publisher=%(publisher)s, published_year=%(published_year)s, ' \
|
|
' edition=%(edition)s, pages=%(pages)s, series=%(series)s, ' \
|
|
' description=%(description)s ' \
|
|
'WHERE isbn=%(isbn)s'
|
|
q_remove_bookpersons = \
|
|
'DELETE FROM bookperson WHERE book=%(isbn)s'
|
|
q_add_bookperson = \
|
|
'INSERT INTO bookperson (book, person, relation) ' \
|
|
'VALUES (%(isbn)s, %(person_id)s, %(relation)s)'
|
|
q_new_category = \
|
|
'INSERT INTO category (id, name) ' \
|
|
'VALUES (%(id)s, %(name)s)'
|
|
q_edit_category = \
|
|
'UPDATE category ' \
|
|
'SET name=%(name)s ' \
|
|
'WHERE id=%(id)s'
|
|
q_add_bookreference = \
|
|
'INSERT INTO bookreference (book, reftype, value) ' \
|
|
'VALUES (%(isbn)s, %(reftype)s, %(value)s)'
|
|
|
|
def connect_to_db():
|
|
connection = pgdb.connect(database='oysteini_pbb2',
|
|
user='oysteini_pbb',
|
|
password='lio5Aide',
|
|
host='postgres.pvv.ntnu.no')
|
|
return connection
|
|
|
|
def get_by_id(connection, id):
|
|
c = connection.cursor()
|
|
q_book = 'SELECT * FROM book WHERE isbn=%(id)s OR id=%(id)s'
|
|
q_person = 'SELECT * FROM person WHERE id=%(id)s'
|
|
q_cat = 'SELECT * FROM category WHERE id=%(id)s'
|
|
for (typ,q) in [('book', q_book),
|
|
('person', q_person),
|
|
('category', q_cat)]:
|
|
execute_query(c, q, {'id': id})
|
|
if c.rowcount > 0:
|
|
d = fetchone_dict(c)
|
|
d['type'] = typ
|
|
return d
|
|
return None
|
|
|
|
def list_of_dicts_to_dict(lst, key_name, value_name):
|
|
res = {}
|
|
for d in lst:
|
|
if d[key_name] in res:
|
|
res[d[key_name]].append(d[value_name])
|
|
else:
|
|
res[d[key_name]] = [d[value_name]]
|
|
return res
|
|
|
|
def get_persons_for_book(connection, isbn):
|
|
c = connection.cursor()
|
|
c.execute(q_persons_for_book, {'isbn': isbn})
|
|
return fetchall_dict(c)
|
|
|
|
def get_books_for_person(connection, person_id):
|
|
c = connection.cursor()
|
|
c.execute(q_books_for_person, {'id': person_id})
|
|
return fetchall_dict(c)
|
|
|
|
def get_books_for_category(connection, cat_id):
|
|
c = connection.cursor()
|
|
c.execute(q_books_for_category, {'id': cat_id})
|
|
return fetchall_dict(c)
|
|
|
|
def show_book(book):
|
|
s = ''
|
|
if book['id']:
|
|
s += 'book %s %s\n' % (book['isbn'], book['id'])
|
|
else:
|
|
s += 'book %s\n' % book['isbn']
|
|
s += 'Title: %s\n' % book['title']
|
|
if book['subtitle']:
|
|
s += 'Subtitle: %s\n' % book['subtitle']
|
|
s += 'ISBN: %s\n' % book['isbn']
|
|
s += 'Persons:\n'
|
|
for bp in book['persons_data']:
|
|
s += ' %s %s %s (%s)\n' % (bp['id'], bp['firstname'], bp['lastname'],
|
|
bp['relation'])
|
|
if len(book['persons_data']) == 0:
|
|
s += ' (no persons associated with this book)\n'
|
|
if book['series']:
|
|
s += 'Part of series: %s %s\n' % (book['series'], book['series_title'])
|
|
s += 'Category: %s\n' % book['category']
|
|
if book['publisher']:
|
|
s += 'Publisher: %s\n' % book['publisher']
|
|
if book['published_year']:
|
|
s += 'Published year: %s\n' % book['published_year']
|
|
if book['edition']:
|
|
s += 'Edition: %s\n' % book['edition']
|
|
if book['pages']:
|
|
s += 'Number of pages: %s\n' % book['pages']
|
|
if book['description']:
|
|
s += ('Description:\n%s\n' %
|
|
'\n'.join(map(lambda line: ' '+line,
|
|
book['description'].split('\n'))))
|
|
return s
|
|
|
|
def show_person(person):
|
|
s = 'person %s\n' % person['id']
|
|
s += 'Name: %s %s\n' % (person['firstname'], person['lastname'])
|
|
s += 'Books:\n'
|
|
for book in person['books']:
|
|
s += ' %-13s %s (%s)\n' % (book['isbn'], book['title'], book['relation'])
|
|
if len(person['books']) == 0:
|
|
s += ' (no books by this person)\n'
|
|
return s
|
|
|
|
def show_category(cat):
|
|
s = 'category %s\n' % cat['id']
|
|
s += 'Name: %s\n' % cat['name']
|
|
s += 'Books:\n'
|
|
for book in cat['books']:
|
|
s += ' %-13s %s\n' % (book['isbn'], book['title'])
|
|
if len(cat['books']) == 0:
|
|
s += ' (no books)\n'
|
|
return s
|
|
|
|
def show(connection, ids, commit_format=False, tmp_file=False):
|
|
objects = map(lambda id: get_by_id(connection, id), ids)
|
|
for i in range(len(ids)):
|
|
if not objects[i]:
|
|
objects[i] = 'No object with id %s.\n' % ids[i]
|
|
continue
|
|
|
|
typ = objects[i]['type']
|
|
if typ == 'book':
|
|
persons = get_persons_for_book(connection, objects[i]['isbn'])
|
|
objects[i]['persons'] = list_of_dicts_to_dict(persons, 'relation', 'id')
|
|
objects[i]['persons_data'] = persons
|
|
elif typ == 'person':
|
|
books = get_books_for_person(connection, objects[i]['id'])
|
|
objects[i]['books'] = books
|
|
elif typ == 'category':
|
|
books = get_books_for_category(connection, objects[i]['id'])
|
|
objects[i]['books'] = books
|
|
if commit_format:
|
|
objects[i]['action'] = 'edit-%s' % typ
|
|
else:
|
|
show_funs = {'book': show_book,
|
|
'person': show_person,
|
|
'category': show_category}
|
|
show_fun = show_funs[objects[i]['type']]
|
|
objects[i] = show_fun(objects[i])
|
|
|
|
if commit_format:
|
|
output = encoding_comment() + write_actionlist(objects)
|
|
else:
|
|
output = '\n'.join(objects)
|
|
if tmp_file:
|
|
with tmpfile('.'.join(ids)):
|
|
write_stderr('%s\n' % tmpfile_name())
|
|
write(output)
|
|
return tmpfile_name()
|
|
else:
|
|
write(output)
|
|
|
|
def list_books(connection):
|
|
c = connection.cursor()
|
|
c.execute(q_list_books)
|
|
#print fetchall_dict(c)
|
|
for i in xrange(c.rowcount):
|
|
book = fetchone_dict(c)
|
|
write('%-13s %-10s %-60s %s\n' %
|
|
(book['isbn'], str_or_empty(book['id']),
|
|
cut_str(book['title'], 60), book['persons']))
|
|
|
|
def list_persons(connection):
|
|
c = connection.cursor()
|
|
c.execute(q_list_persons)
|
|
for i in xrange(c.rowcount):
|
|
person = fetchone_dict(c)
|
|
write('%-5s %-30s %d books\n' % (person['id'],
|
|
person['firstname']+' '+person['lastname'],
|
|
person['num_books']))
|
|
|
|
def list_categories(connection):
|
|
c = connection.cursor()
|
|
c.execute(q_list_categories)
|
|
for i in xrange(c.rowcount):
|
|
cat = fetchone_dict(c)
|
|
write('%-15s %-30s %d books\n' % (cat['id'], cat['name'], cat['num_books']))
|
|
|
|
def list_cmd(connection, what):
|
|
funs = { 'book': list_books,
|
|
'person': list_persons,
|
|
'category': list_categories }
|
|
fun = funs[what]
|
|
fun(connection)
|
|
|
|
def search_book(connection, search_strings, search_description=False):
|
|
c = connection.cursor()
|
|
if search_description:
|
|
where_clauses = ['book.title ILIKE %s OR book.subtitle ILIKE %s OR book.series ILIKE %s \
|
|
OR person.lastname ILIKE %s OR person.firstname ILIKE %s OR book.description ILIKE %s']*len(search_strings)
|
|
else:
|
|
where_clauses = ['book.title ILIKE %s OR book.subtitle ILIKE %s OR book.series ILIKE %s \
|
|
OR person.lastname ILIKE %s OR person.firstname ILIKE %s']*len(search_strings)
|
|
|
|
result_list = []
|
|
for s in search_strings:
|
|
if search_description:
|
|
for i in range(6):
|
|
result_list.append(s)
|
|
else:
|
|
for i in range(5):
|
|
result_list.append(s)
|
|
c.execute('SELECT isbn,book.id AS id,title,category, \
|
|
array_to_string(array_agg(person.lastname || \' (\' || person.id || \')\'), \', \') AS persons \
|
|
FROM book LEFT JOIN bookperson ON book.isbn=bookperson.book \
|
|
LEFT JOIN person ON person.id=bookperson.person \
|
|
WHERE ' + ' OR '.join(where_clauses) + '\
|
|
GROUP BY isbn, book.id, title, category \
|
|
', map(lambda s:'%' + s + '%',result_list))
|
|
for i in xrange(c.rowcount):
|
|
book = fetchone_dict(c)
|
|
write('%-13s %-10s %-60s %s' %
|
|
(book['isbn'], str_or_empty(book['id']),
|
|
cut_str(book['title'], 60), book['persons']))
|
|
|
|
|
|
def search_person(connection, search_strings):
|
|
c = connection.cursor()
|
|
result_strings = []
|
|
for s in search_strings:
|
|
for i in range(3):
|
|
result_strings.append(s)
|
|
c.execute('SELECT * FROM person LEFT JOIN bookperson ON person.id=bookperson.person \
|
|
WHERE person.lastname ILIKE %s or person.firstname ILIKE %s OR person.id ILIKE %s', result_strings)
|
|
for i in xrange(c.rowcount):
|
|
person = fetchone_dict(c)
|
|
write(person['lastname'], ', ', person['firstname'], '\t', person['book'])
|
|
|
|
def do_action(connection, action):
|
|
debug('ACTION %s ' % action)
|
|
c = connection.cursor()
|
|
queries = {'new-person': q_new_person,
|
|
'edit-person': q_edit_person,
|
|
'new-book': q_new_book,
|
|
'edit-book': q_edit_book,
|
|
'new-category': q_new_category,
|
|
'edit-category': q_edit_category}
|
|
action_type = action['action']
|
|
execute_query(c, queries[action_type], action)
|
|
if action_type in ['new-book', 'edit-book']:
|
|
debug('FIXING PERSONS: REMOVING')
|
|
c.execute(q_remove_bookpersons, {'isbn': action['isbn']})
|
|
debug('FIXING PERSONS: ADDING')
|
|
if action['persons']:
|
|
for (relation, personlist) in action['persons'].items():
|
|
for person in personlist:
|
|
c.execute(q_add_bookperson,
|
|
{'isbn': action['isbn'],
|
|
'person_id': person,
|
|
'relation': relation})
|
|
if action['references']:
|
|
c.execute('SELECT referencetype.id FROM referencetype')
|
|
legal_reftypes = [a for list in c.fetchall() for a in list]
|
|
for (reftype, reflist) in action['references'].items():
|
|
for ref in reflist:
|
|
if reftype in legal_reftypes:
|
|
c.execute(q_add_bookreference,
|
|
{'isbn': action['isbn'],
|
|
'reftype': reftype,
|
|
'value': ref})
|
|
else:
|
|
raise WorblehatException('%s is not in the defined references, please use a more general one' % reftype)
|
|
|
|
def commit_actions(connection, actions):
|
|
for action in actions:
|
|
try:
|
|
do_action(connection, action)
|
|
except pgdb.DatabaseError, err:
|
|
raise WorblehatException('commit: Error in "%s" action: %s' % (action['action'], err))
|
|
connection.commit()
|
|
|
|
def commit(connection, filename=None):
|
|
if filename:
|
|
try:
|
|
f = file(filename, 'r')
|
|
text = f.read()
|
|
f.close()
|
|
except IOError, e:
|
|
raise WorblehatException('commit: Error reading file %s: %s' % (filename, e))
|
|
else:
|
|
text = sys.stdin.read()
|
|
|
|
actions = read_actionlist(text)
|
|
commit_actions(connection, actions)
|
|
|
|
def edit(connection, ids):
|
|
filename = show(connection, ids, commit_format=True, tmp_file=True)
|
|
done = False
|
|
while not done:
|
|
run_editor(filename)
|
|
try:
|
|
commit(connection, filename)
|
|
done = True
|
|
except WorblehatException, exc:
|
|
write_stderr('%s\n' % exc)
|
|
answer = raw_input('Retry? [Y/n] ')
|
|
if answer == 'n':
|
|
done = True
|
|
|
|
def map_cmd(connection, shelfname=None, category=None):
|
|
pass
|
|
|
|
def suggest_book_data(connection, tmp_file=False):
|
|
return google_suggest_book_data(connection, tmp_file)
|
|
|
|
def register_books(connection):
|
|
filename = suggest_book_data(connection, tmp_file=True)
|
|
#print("Tempfile filename: " + filename)
|
|
run_editor(filename)
|
|
commit(connection, filename)
|
|
|
|
def give_bananas():
|
|
write("Om nom nom... Thanks!")
|
|
|
|
commands = { 'show':
|
|
{ 'args': [('ids', (1,None))],
|
|
'options': ['commit_format', 'tmp_file'],
|
|
'fun': show,
|
|
'use_db': True },
|
|
'list':
|
|
{ 'args': [('what', (1,1))],
|
|
'options': [],
|
|
'fun': list_cmd,
|
|
'use_db': True },
|
|
'search':
|
|
{ 'args': [('search_strings', (1,None))],
|
|
'options': ['search_description'],
|
|
'fun': search_book,
|
|
'use_db': True },
|
|
'search-person':
|
|
{ 'args': [('search_strings', (1,None))],
|
|
'options': [],
|
|
'fun': search_person,
|
|
'use_db': True },
|
|
'commit':
|
|
{ 'args': [('filename', (0,1))],
|
|
'options': [],
|
|
'fun': commit,
|
|
'use_db': True },
|
|
'edit':
|
|
{ 'args': [('ids', (1,None))],
|
|
'options': [],
|
|
'fun': edit,
|
|
'use_db': True },
|
|
'map':
|
|
{ 'args': [('shelfname', (0,1)), ('category', (0,1))],
|
|
'options': [],
|
|
'fun': map_cmd,
|
|
'use_db': True },
|
|
'suggest-book-data':
|
|
{ 'args': [],
|
|
'options': ['tmp_file'],
|
|
'fun': suggest_book_data,
|
|
'use_db': True },
|
|
'register-books':
|
|
{ 'args': [],
|
|
'options': [],
|
|
'fun': register_books,
|
|
'use_db': True },
|
|
'give-bananas':
|
|
{ 'args': [],
|
|
'options': [],
|
|
'fun': give_bananas,
|
|
'use_db': False }
|
|
}
|
|
|
|
flags = { 'commit_format':
|
|
{ 'help': 'output data in the format expected by the commit command' },
|
|
'tmp_file':
|
|
{ 'help': 'output data to a new temporary file instead of to stdout' },
|
|
'search_description':
|
|
{ 'help': 'include description field when searching' }
|
|
}
|
|
|
|
general_options = [] # options applicable to all commands
|
|
|
|
class BadCommandLine(Exception):
|
|
def __init__(self, msg):
|
|
Exception.__init__(self, 'Bad command line: ' + msg)
|
|
|
|
def check_command_args(args, command):
|
|
cmd_decl = commands[command]
|
|
min_num_args = sum(map(lambda a: a[1][0], cmd_decl['args']))
|
|
unlimited = any(map(lambda a: a[1][1] == None, cmd_decl['args']))
|
|
if not unlimited:
|
|
max_num_args = sum(map(lambda a: a[1][1], cmd_decl['args']))
|
|
if len(args) < min_num_args:
|
|
raise BadCommandLine('Too few arguments for command %s (expects at least %d, %d given).'
|
|
% (command, min_num_args, len(args)))
|
|
if (not unlimited) and (len(args) > max_num_args):
|
|
raise BadCommandLine('Too many arguments for command %s (expects at most %d, %d given).'
|
|
% (command, max_num_args, len(args)))
|
|
|
|
|
|
def check_command_opts(opts, command):
|
|
cmd_decl = commands[command]
|
|
for opt,val in opts:
|
|
if ((opt not in cmd_decl['options']) and
|
|
(opt not in general_options)):
|
|
raise BadCommandLine('Option %s not applicable to command %s.'
|
|
% (opt, command))
|
|
|
|
def assign_command_args(args, command):
|
|
cmd_decl = commands[command]
|
|
d = {}
|
|
i = 0
|
|
for param in cmd_decl['args']:
|
|
pname = param[0]
|
|
pmin = param[1][0]
|
|
pmax = param[1][1]
|
|
if pmax == 1:
|
|
if i < len(args):
|
|
d[pname] = args[i]
|
|
i += 1
|
|
else:
|
|
d[pname] = None
|
|
else:
|
|
d[pname] = []
|
|
j = 0
|
|
while i + j < len(args) and (pmax == None or j < pmax):
|
|
d[pname].append(args[i + j])
|
|
j += 1
|
|
i = i + j
|
|
return d
|
|
|
|
def assign_command_opts(opts, command):
|
|
d = {}
|
|
for option, value in opts:
|
|
if option in flags:
|
|
d[option] = True
|
|
else:
|
|
d[option] = value
|
|
return d
|
|
|
|
def parse_cmdline(args):
|
|
def getopt_option_name(internal_name):
|
|
return internal_name.replace('_', '-')
|
|
def internal_option_name(getopt_ret_name):
|
|
return getopt_ret_name[2:].replace('-', '_')
|
|
|
|
option_names = map(getopt_option_name, flags)
|
|
|
|
options, args = getopt.getopt(args, '', option_names)
|
|
|
|
if len(args) == 0:
|
|
raise BadCommandLine('No command specified.')
|
|
cmd_name = args[0]
|
|
if cmd_name not in commands:
|
|
raise BadCommandLine('Nonexisting command %s.' % cmd_name)
|
|
cmd_decl = commands[cmd_name]
|
|
cmd_args = args[1:]
|
|
cmd_opts = map(lambda (opt,val): (internal_option_name(opt), val),
|
|
options)
|
|
|
|
check_command_args(cmd_args, cmd_name)
|
|
check_command_opts(cmd_opts, cmd_name)
|
|
|
|
return { 'command': cmd_name,
|
|
'args': combine_dicts(assign_command_args(cmd_args, cmd_name),
|
|
assign_command_opts(cmd_opts, cmd_name)) }
|
|
|
|
def invoke_command(command, args):
|
|
cmd_decl = commands[command]
|
|
cmd_decl['fun'](**args)
|
|
|
|
def main(argv):
|
|
cmdline_parsed = parse_cmdline(argv[1:])
|
|
#print 'command line parsed to:', cmdline_parsed
|
|
command = cmdline_parsed['command']
|
|
args = cmdline_parsed['args']
|
|
if commands[cmdline_parsed['command']]['use_db']:
|
|
connection = connect_to_db()
|
|
args['connection'] = connection
|
|
invoke_command(command, args)
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv)
|
|
|