From a4dbf3b77c7d28f57c56c74155f6a91e79d8c3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=A1clav=20=27Ax=27=20H=C5=AFla?= Date: Sat, 27 Oct 2012 23:20:43 +0200 Subject: [PATCH] All db operations now should go thru Database object --- brmbar3/brmbar-gui-qt4.py | 4 ++- brmbar3/brmbar/Account.py | 47 +++++++++++---------------- brmbar3/brmbar/Currency.py | 66 ++++++++++++++++---------------------- brmbar3/brmbar/Database.py | 50 +++++++++++++++++++++++++++++ brmbar3/brmbar/Shop.py | 61 +++++++++++++++++------------------ 5 files changed, 128 insertions(+), 100 deletions(-) create mode 100644 brmbar3/brmbar/Database.py diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index a3be7f1..095f0e2 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -5,6 +5,8 @@ import psycopg2 from PySide import QtCore, QtGui, QtDeclarative +from brmbar import Database + import brmbar @@ -173,7 +175,7 @@ class ShopAdapter(QtCore.QObject): db.commit() return balance -db = psycopg2.connect("dbname=brmbar") +db = Database.Database("dbname=brmbar") shop = brmbar.Shop.new_with_defaults(db) currency = shop.currency db.commit() diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index a1de8e2..412b507 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -17,31 +17,25 @@ class Account: @classmethod def load_by_barcode(cls, db, barcode): - with closing(db.cursor()) as cur: - cur.execute("SELECT account FROM barcodes WHERE barcode = %s", [barcode]) - res = cur.fetchone() - if res is None: - return None - id = res[0] + res = db.execute_and_fetch("SELECT account FROM barcodes WHERE barcode = %s", [barcode]) + if res is None: + return None + id = res[0] return cls.load(db, id = id) @classmethod def load(cls, db, id = None, name = None): """ Constructor for existing account """ if id is not None: - with closing(db.cursor()) as cur: - cur.execute("SELECT name FROM accounts WHERE id = %s", [id]) - name = cur.fetchone()[0] + name = db.execute_and_fetch("SELECT name FROM accounts WHERE id = %s", [id]) + name = name[0] elif name is not None: - with closing(db.cursor()) as cur: - cur.execute("SELECT id FROM accounts WHERE name = %s", [name]) - id = cur.fetchone()[0] + id = db.execute_and_fetch("SELECT id FROM accounts WHERE name = %s", [name]) + id = id[0] else: raise NameError("Account.load(): Specify either id or name") - with closing(db.cursor()) as cur: - cur.execute("SELECT currency, acctype FROM accounts WHERE id = %s", [id]) - currid, acctype = cur.fetchone() + currid, acctype = db.execute_and_fetch("SELECT currency, acctype FROM accounts WHERE id = %s", [id]) currency = Currency.load(db, id = currid) return cls(db, name = name, id = id, currency = currency, acctype = acctype) @@ -49,17 +43,15 @@ class Account: @classmethod def create(cls, db, name, currency, acctype): """ Constructor for new account """ - with closing(db.cursor()) as cur: - cur.execute("INSERT INTO accounts (name, currency, acctype) VALUES (%s, %s, %s) RETURNING id", [name, currency.id, acctype]) - id = cur.fetchone()[0] + id = db.execute_and_fetch("INSERT INTO accounts (name, currency, acctype) VALUES (%s, %s, %s) RETURNING id", [name, currency.id, acctype]) + id = id[0] return cls(db, name = name, id = id, currency = currency, acctype = acctype) def balance(self): - with closing(self.db.cursor()) as cur: - cur.execute("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'debit']) - debit = cur.fetchone()[0] or 0 - cur.execute("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'credit']) - credit = cur.fetchone()[0] or 0 + debit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'debit']) + debit = debit[0] or 0 + credit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'credit']) + credit = credit[0] or 0 return debit - credit def balance_str(self): @@ -76,15 +68,12 @@ class Account: def _transaction_split(self, transaction, side, amount, memo): """ Common part of credit() and debit(). """ - with closing(self.db.cursor()) as cur: - cur.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo]) + self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo]) def add_barcode(self, barcode): - with closing(self.db.cursor()) as cur: - cur.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) + self.db.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) self.db.commit() def rename(self, name): - with closing(self.db.cursor()) as cur: - cur.execute("UPDATE accounts SET name = %s WHERE id = %s", [name, self.id]) + self.db.execute("UPDATE accounts SET name = %s WHERE id = %s", [name, self.id]) self.name = name diff --git a/brmbar3/brmbar/Currency.py b/brmbar3/brmbar/Currency.py index ba818e4..36f5934 100644 --- a/brmbar3/brmbar/Currency.py +++ b/brmbar3/brmbar/Currency.py @@ -20,13 +20,11 @@ class Currency: def load(cls, db, id = None, name = None): """ Constructor for existing currency """ if id is not None: - with closing(db.cursor()) as cur: - cur.execute("SELECT name FROM currencies WHERE id = %s", [id]) - name = cur.fetchone()[0] + name = db.execute_and_fetch("SELECT name FROM currencies WHERE id = %s", [id]) + name = name[0] elif name is not None: - with closing(db.cursor()) as cur: - cur.execute("SELECT id FROM currencies WHERE name = %s", [name]) - id = cur.fetchone()[0] + id = db.execute_and_fetch("SELECT id FROM currencies WHERE name = %s", [name]) + id = id[0] else: raise NameError("Currency.load(): Specify either id or name") return cls(db, name = name, id = id) @@ -34,52 +32,44 @@ class Currency: @classmethod def create(cls, db, name): """ Constructor for new currency """ - with closing(db.cursor()) as cur: - cur.execute("INSERT INTO currencies (name) VALUES (%s) RETURNING id", [name]) - id = cur.fetchone()[0] + id = db.execute_and_fetch("INSERT INTO currencies (name) VALUES (%s) RETURNING id", [name]) + id = id[0] return cls(db, name = name, id = id) def rates(self, other): """ Return tuple ($buy, $sell) of rates of $self in relation to $other (brmbar.Currency): $buy is the price of $self in means of $other when buying it (into brmbar) $sell is the price of $self in means of $other when selling it (from brmbar) """ - with closing(self.db.cursor()) as cur: - cur.execute("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [self.id, other.id]) - res = cur.fetchone() - if res is None: - raise NameError("Currency.rate(): Unknown conversion " + other.name() + " to " + self.name()) - buy_rate, buy_rate_dir = res - buy = buy_rate if buy_rate_dir == "target_to_source" else 1/buy_rate + res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [self.id, other.id]) + if res is None: + raise NameError("Currency.rate(): Unknown conversion " + other.name() + " to " + self.name()) + buy_rate, buy_rate_dir = res + buy = buy_rate if buy_rate_dir == "target_to_source" else 1/buy_rate - cur.execute("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [other.id, self.id]) - res = cur.fetchone() - if res is None: - raise NameError("Currency.rate(): Unknown conversion " + self.name() + " to " + other.name()) - sell_rate, sell_rate_dir = res - sell = sell_rate if sell_rate_dir == "source_to_target" else 1/sell_rate + res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [other.id, self.id]) + if res is None: + raise NameError("Currency.rate(): Unknown conversion " + self.name() + " to " + other.name()) + sell_rate, sell_rate_dir = res + sell = sell_rate if sell_rate_dir == "source_to_target" else 1/sell_rate - return (buy, sell) + return (buy, sell) def convert(self, amount, target): - with closing(self.db.cursor()) as cur: - cur.execute("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [target.id, self.id]) - res = cur.fetchone() - if res is None: - raise NameError("Currency.convert(): Unknown conversion " + self.name() + " to " + other.name()) - rate, rate_dir = res - if rate_dir == "source_to_target": - resamount = amount * rate - else: - resamount = amount / rate - return resamount + res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [target.id, self.id]) + if res is None: + raise NameError("Currency.convert(): Unknown conversion " + self.name() + " to " + target.name()) + rate, rate_dir = res + if rate_dir == "source_to_target": + resamount = amount * rate + else: + resamount = amount / rate + return resamount def str(self, amount): return "{:.2f} {}".format(amount, self.name) def update_sell_rate(self, target, rate): - with closing(self.db.cursor()) as cur: - cur.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [self.id, target.id, rate, "source_to_target"]) + self.db.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [self.id, target.id, rate, "source_to_target"]) def update_buy_rate(self, source, rate): - with closing(self.db.cursor()) as cur: - cur.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [source.id, self.id, rate, "target_to_source"]) + self.db.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [source.id, self.id, rate, "target_to_source"]) diff --git a/brmbar3/brmbar/Database.py b/brmbar3/brmbar/Database.py new file mode 100644 index 0000000..797bc59 --- /dev/null +++ b/brmbar3/brmbar/Database.py @@ -0,0 +1,50 @@ +"""self-reconnecting database object""" + +import psycopg2 +from contextlib import closing +import time + +class Database: + """self-reconnecting database object""" + def __init__(self, dsn): + self.db_conn = psycopg2.connect(dsn) + self.dsn = dsn + + def execute(self, query, attrs = None): + """execute a query and return one result""" + with closing(self.db_conn.cursor()) as cur: + cur = self._execute(cur, query, attrs) + + def execute_and_fetch(self, query, attrs = None): + """execute a query and return one result""" + with closing(self.db_conn.cursor()) as cur: + cur = self._execute(cur, query, attrs) + return cur.fetchone() + + def execute_and_fetchall(self, query, attrs = None): + """execute a query and return all results""" + with closing(self.db_conn.cursor()) as cur: + cur = self._execute(cur, query, attrs) + return cur.fetchall() + + def _execute(self, cur, query, attrs, level=1): + """execute a query, and in case of OperationalError (db restart) + reconnect to database. Recurses with increasig pause between tries""" + try: + if attrs is None: + cur.execute(query) + else: + cur.execute(query, attrs) + return cur + except psycopg2.OperationalError as error: + print("Sleeping: level %s (%s) @%s" % ( + level, error, time.strftime("%Y%m%d %a %I:%m %p") + )) + time.sleep(2 ** level) + self.db_conn = psycopg2.connect(self.dsn) + cur = self.db_conn.cursor() #how ugly is this? + return self._execute(cur, query, attrs, level+1) + + def commit(self): + """passes commit to db""" + self.db_conn.commit() diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index d689445..f709c33 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -82,51 +82,48 @@ class Shop: self.db.commit() def _transaction(self, responsible = None, description = None): - with closing(self.db.cursor()) as cur: - cur.execute("INSERT INTO transactions (responsible, description) VALUES (%s, %s) RETURNING id", - [responsible.id if responsible else None, description]) - transaction = cur.fetchone()[0] + transaction = self.db.execute_and_fetch("INSERT INTO transactions (responsible, description) VALUES (%s, %s) RETURNING id", + [responsible.id if responsible else None, description]) + transaction = transaction[0] return transaction def credit_balance(self): - with closing(self.db.cursor()) as cur: - # We assume all debt accounts share a currency - sumselect = """ - SELECT SUM(ts.amount) - FROM accounts AS a - LEFT JOIN transaction_splits AS ts ON a.id = ts.account - WHERE a.acctype = %s AND ts.side = %s - """ - cur.execute(sumselect, ["debt", 'debit']) - debit = cur.fetchone()[0] or 0 - cur.execute(sumselect, ["debt", 'credit']) - credit = cur.fetchone()[0] or 0 + # We assume all debt accounts share a currency + sumselect = """ + SELECT SUM(ts.amount) + FROM accounts AS a + LEFT JOIN transaction_splits AS ts ON a.id = ts.account + WHERE a.acctype = %s AND ts.side = %s + """ + cur = self.db.execute_and_fetch(sumselect, ["debt", 'debit']) + debit = cur[0] or 0 + credit = self.db.execute_and_fetch(sumselect, ["debt", 'credit']) + credit = credit[0] or 0 return debit - credit def credit_negbalance_str(self): return self.currency.str(-self.credit_balance()) def inventory_balance(self): balance = 0 - with closing(self.db.cursor()) as cur: - # Each inventory account has its own currency, - # so we just do this ugly iteration - cur.execute("SELECT id FROM accounts WHERE acctype = %s", ["inventory"]) - for inventory in cur: - invid = inventory[0] - inv = Account.load(self.db, id = invid) - # FIXME: This is not correct as each instance of inventory - # might have been bought for a different price! Therefore, - # we need to replace the command below with a complex SQL - # statement that will... ugh, accounting is hard! - balance += inv.currency.convert(inv.balance(), self.currency) + # Each inventory account has its own currency, + # so we just do this ugly iteration + cur = self.db.execute_and_fetchall("SELECT id FROM accounts WHERE acctype = %s", ["inventory"]) + for inventory in cur: + invid = inventory[0] + inv = Account.load(self.db, id = invid) + # FIXME: This is not correct as each instance of inventory + # might have been bought for a different price! Therefore, + # we need to replace the command below with a complex SQL + # statement that will... ugh, accounting is hard! + balance += inv.currency.convert(inv.balance(), self.currency) return balance def inventory_balance_str(self): return self.currency.str(self.inventory_balance()) def account_list(self, acctype): + """list all accounts (people or items, as per acctype)""" accts = [] - with closing(self.db.cursor()) as cur: - cur.execute("SELECT id FROM accounts WHERE acctype = %s ORDER BY name ASC", [acctype]) - for inventory in cur: - accts += [ Account.load(self.db, id = inventory[0]) ] + cur = self.db.execute_and_fetchall("SELECT id FROM accounts WHERE acctype = %s ORDER BY name ASC", [acctype]) + for inventory in cur: + accts += [ Account.load(self.db, id = inventory[0]) ] return accts