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