From 124bd1706d792f3dab0f2f44739287a7d98682df Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 14 Dec 2013 16:08:47 +0100 Subject: [PATCH] Added SQL storage backend - Added Storage ABC - Moved Ledger class to storage/ledgercli.py - Added SQL requirements to requirements.txt --- accounting/__init__.py | 320 ---------------------------- accounting/client.py | 19 +- accounting/config.py | 3 + accounting/models.py | 7 +- accounting/storage/__init__.py | 19 ++ accounting/storage/ledgercli.py | 324 +++++++++++++++++++++++++++++ accounting/storage/sql/__init__.py | 72 +++++++ accounting/storage/sql/models.py | 53 +++++ accounting/web.py | 44 ++-- 9 files changed, 505 insertions(+), 356 deletions(-) create mode 100644 accounting/storage/__init__.py create mode 100644 accounting/storage/ledgercli.py create mode 100644 accounting/storage/sql/__init__.py create mode 100644 accounting/storage/sql/models.py diff --git a/accounting/__init__.py b/accounting/__init__.py index 7dd24a9..e69de29 100644 --- a/accounting/__init__.py +++ b/accounting/__init__.py @@ -1,320 +0,0 @@ -import sys -import subprocess -import logging -import time - -from datetime import datetime -from xml.etree import ElementTree -from contextlib import contextmanager - -from accounting.models import Account, Transaction, Posting, Amount - -_log = logging.getLogger(__name__) - -class Ledger: - def __init__(self, ledger_file=None, ledger_bin=None): - if ledger_file is None: - raise ValueError('ledger_file cannot be None') - - self.ledger_bin = ledger_bin or 'ledger' - self.ledger_file = ledger_file - _log.info('ledger file: %s', ledger_file) - - self.locked = False - self.ledger_process = None - - @contextmanager - def locked_process(self): - r''' - Context manager that checks that the ledger process is not already - locked, then "locks" the process and yields the process handle and - unlocks the process when execution is returned. - - Since this decorated as a :func:`contextlib.contextmanager` the - recommended use is with the ``with``-statement. - - .. code-block:: python - - with self.locked_process() as p: - p.stdin.write(b'bal\n') - - output = self.read_until_prompt(p) - - ''' - if self.locked: - raise RuntimeError('The process has already been locked,' - ' something\'s out of order.') - - # XXX: This code has no purpose in a single-threaded process - timeout = 5 # Seconds - - for i in range(1, timeout + 2): - if i > timeout: - raise RuntimeError('Ledger process is already locked') - - if not self.locked: - break - else: - _log.info('Waiting for one second... %d/%d', i, timeout) - time.sleep(1) - - process = self.get_process() - - self.locked = True - _log.debug('Lock enabled') - - yield process - - self.locked = False - _log.debug('Lock disabled') - - def assemble_arguments(self): - ''' - Returns a list of arguments suitable for :class:`subprocess.Popen` based on - :attr:`self.ledger_bin` and :attr:`self.ledger_file`. - ''' - return [ - self.ledger_bin, - '-f', - self.ledger_file, - ] - - def init_process(self): - ''' - Creates a new (presumably) ledger subprocess based on the args from - :meth:`Ledger.assemble_arguments()` and then runs - :meth:`Ledger.read_until_prompt()` once (which should return the banner - text) and discards the output. - ''' - _log.debug('Starting ledger process...') - self.ledger_process = subprocess.Popen( - self.assemble_arguments(), - stdout=subprocess.PIPE, - stdin=subprocess.PIPE, - stderr=subprocess.PIPE) - - # Swallow the banner - with self.locked_process() as p: - self.read_until_prompt(p) - - return self.ledger_process - - def get_process(self): - ''' - Returns :attr:`self.ledger_process` if it evaluates to ``True``. If - :attr:`self.ledger_process` is not set the result of - :meth:`self.init_process() ` is returned. - ''' - return self.ledger_process or self.init_process() - - def read_until_prompt(self, process): - r''' - Reads from the subprocess instance :data:`process` until it finds a - combination of ``\n]\x20`` (the prompt), then returns the output - without the prompt. - ''' - output = b'' - - while True: - line = process.stdout.read(1) # XXX: This is a hack - - output += line - - if b'\n] ' in output: - _log.debug('Found prompt!') - break - - output = output[:-3] # Cut away the prompt - - _log.debug('output: %s', output) - - return output - - def send_command(self, command): - output = None - - with self.locked_process() as p: - if isinstance(command, str): - command = command.encode('utf8') - - p.stdin.write(command + b'\n') - p.stdin.flush() - - output = self.read_until_prompt(p) - - self.ledger_process.send_signal(subprocess.signal.SIGTERM) - _log.debug('Waiting for ledger to shut down') - self.ledger_process.wait() - self.ledger_process = None - - return output - - def add_transaction(self, transaction): - ''' - Writes a transaction to the ledger file by opening it in 'ab' mode and - writing a ledger transaction based on the - :class:`~accounting.models.Transaction` instance in :data:`transaction`. - ''' - if not transaction.metadata.get('Id'): - transaction.generate_id() - - transaction_template = ('\n{date} {t.payee}\n' - '{tags}' - '{postings}') - - metadata_template = ' ;{0}: {1}\n' - - # TODO: Generate metadata for postings - posting_template = (' {account} {p.amount.symbol}' - ' {p.amount.amount}\n') - - output = b'' - - # XXX: Even I hardly understands what this does, however I indent it it - # stays unreadable. - output += transaction_template.format( - date=transaction.date.strftime('%Y-%m-%d'), - t=transaction, - tags=''.join([ - metadata_template.format(k, v) \ - for k, v in transaction.metadata.items()]), - postings=''.join([posting_template.format( - p=p, - account=p.account + ' ' * ( - 80 - (len(p.account) + len(p.amount.symbol) + - len(str(p.amount.amount)) + 1 + 2) - )) for p in transaction.postings - ]) - ).encode('utf8') - - with open(self.ledger_file, 'ab') as f: - f.write(output) - - _log.debug('written to file: %s', output) - - def bal(self): - output = self.send_command('xml') - - if output is None: - raise RuntimeError('bal call returned no output') - - accounts = [] - - xml = ElementTree.fromstring(output.decode('utf8')) - - accounts = self._recurse_accounts(xml.find('./accounts')) - - return accounts - - def _recurse_accounts(self, root): - accounts = [] - - for account in root.findall('./account'): - name = account.find('./fullname').text - - amounts = [] - - # Try to find an account total value, then try to find the account - # balance - account_amounts = account.findall( - './account-total/balance/amount') or \ - account.findall('./account-amount/amount') or \ - account.findall('./account-total/amount') - - if account_amounts: - for amount in account_amounts: - quantity = amount.find('./quantity').text - symbol = amount.find('./commodity/symbol').text - - amounts.append(Amount(amount=quantity, symbol=symbol)) - else: - _log.warning('Account %s does not have any amounts', name) - - accounts.append(Account(name=name, - amounts=amounts, - accounts=self._recurse_accounts(account))) - - return accounts - - def reg(self): - output = self.send_command('xml') - - if output is None: - raise RuntimeError('reg call returned no output') - - entries = [] - - reg_xml = ElementTree.fromstring(output.decode('utf8')) - - for transaction in reg_xml.findall('./transactions/transaction'): - date = datetime.strptime(transaction.find('./date').text, - '%Y/%m/%d') - payee = transaction.find('./payee').text - - postings = [] - - for posting in transaction.findall('./postings/posting'): - account = posting.find('./account/name').text - amount = posting.find('./post-amount/amount/quantity').text - symbol = posting.find( - './post-amount/amount/commodity/symbol').text - - # Get the posting metadata - metadata = {} - - values = posting.findall('./metadata/value') - if values: - for value in values: - key = value.get('key') - value = value.find('./string').text - - _log.debug('metadata: %s: %s', key, value) - - metadata.update({key: value}) - - postings.append( - Posting(account=account, - metadata=metadata, - amount=Amount(amount=amount, symbol=symbol))) - - # Get the transaction metadata - metadata = {} - - values = transaction.findall('./metadata/value') - if values: - for value in values: - key = value.get('key') - value = value.find('./string').text - - _log.debug('metadata: %s: %s', key, value) - - metadata.update({key: value}) - - # Add a Transaction instance to the list - entries.append( - Transaction(date=date, payee=payee, postings=postings, - metadata=metadata)) - - return entries - - -def main(argv=None): - import argparse - if argv is None: - argv = sys.argv - - parser = argparse.ArgumentParser() - parser.add_argument('-v', '--verbosity', - default='INFO', - help=('Filter logging output. Possible values:' + - ' CRITICAL, ERROR, WARNING, INFO, DEBUG')) - - args = parser.parse_args(argv[1:]) - logging.basicConfig(level=getattr(logging, args.verbosity, 'INFO')) - ledger = Ledger(ledger_file='non-profit-test-data.ledger') - print(ledger.bal()) - print(ledger.reg()) - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/accounting/client.py b/accounting/client.py index 9486eb1..450cb63 100644 --- a/accounting/client.py +++ b/accounting/client.py @@ -58,9 +58,9 @@ class Client: return self.post('/transaction', {'transactions': [t]}) def get_register(self): - register = self.get('/register') + register = self.get('/transaction') - return register['register_report'] + return register['transactions'] def print_transactions(transactions): @@ -71,18 +71,21 @@ def print_transactions(transactions): for posting in transaction.postings: print(' ' + posting.account + - ' ' * (80 - len(posting.account) - len(posting.amount.symbol) - - len(str(posting.amount.amount)) - 1 - 1) + - posting.amount.symbol + ' ' + str(posting.amount.amount)) + ' ' * (80 - len(posting.account) - + len(posting.amount.symbol) - + len(str(posting.amount.amount)) - 1 - 1) + + posting.amount.symbol + ' ' + str(posting.amount.amount)) def print_balance_accounts(accounts, level=0): for account in accounts: print(' ' * level + ' + {account.name}'.format(account=account) + ' ' + '-' * (80 - len(str(account.name)) - level)) + for amount in account.amounts: print(' ' * level + ' {amount.symbol} {amount.amount}'.format( amount=amount)) + print_balance_accounts(account.accounts, level+1) @@ -101,14 +104,14 @@ def main(argv=None, prog=None): insert.add_argument('to_account') insert.add_argument('amount', type=Decimal) - balance = actions.add_parser('balance', aliases=['bal']) + actions.add_parser('balance', aliases=['bal']) - register = actions.add_parser('register', aliases=['reg']) + actions.add_parser('register', aliases=['reg']) parser.add_argument('-v', '--verbosity', default='WARNING', help=('Filter logging output. Possible values:' + - ' CRITICAL, ERROR, WARNING, INFO, DEBUG')) + ' CRITICAL, ERROR, WARNING, INFO, DEBUG')) parser.add_argument('--host', default='http://localhost:5000') args = parser.parse_args(argv) diff --git a/accounting/config.py b/accounting/config.py index f6b115f..6f4243d 100644 --- a/accounting/config.py +++ b/accounting/config.py @@ -4,3 +4,6 @@ LEDGER_FILE = os.environ.get('LEDGER_FILE', None) DEBUG = bool(int(os.environ.get('DEBUG', 0))) PORT = int(os.environ.get('PORT', 5000)) HOST = os.environ.get('HOST', '127.0.0.1') +SQLALCHEMY_DATABASE_URI = os.environ.get( + 'DATABASE_URI', + 'sqlite:///../accounting-api.sqlite') diff --git a/accounting/models.py b/accounting/models.py index 4e37984..97e7c32 100644 --- a/accounting/models.py +++ b/accounting/models.py @@ -3,8 +3,9 @@ from decimal import Decimal class Transaction: - def __init__(self, date=None, payee=None, postings=None, metadata=None, - _generate_id=False): + def __init__(self, id=None, date=None, payee=None, postings=None, + metadata=None, _generate_id=False): + self.id = id self.date = date self.payee = payee self.postings = postings @@ -14,7 +15,7 @@ class Transaction: self.generate_id() def generate_id(self): - self.metadata.update({'Id': uuid.uuid4()}) + self.id = uuid.uuid4() def __repr__(self): return ('<{self.__class__.__name__} {date}' + diff --git a/accounting/storage/__init__.py b/accounting/storage/__init__.py new file mode 100644 index 0000000..1403245 --- /dev/null +++ b/accounting/storage/__init__.py @@ -0,0 +1,19 @@ + +class Storage: + ''' + ABC for accounting storage + ''' + def __init__(self, *args, **kw): + raise NotImplementedError() + + def get_transactions(self, *args, **kw): + raise NotImplementedError() + + def get_transaction(self, *args, **kw): + raise NotImplementedError() + + def get_account(self, *args, **kw): + raise NotImplementedError() + + def get_accounts(self, *args, **kw): + raise NotImplementedError() diff --git a/accounting/storage/ledgercli.py b/accounting/storage/ledgercli.py new file mode 100644 index 0000000..4a95b89 --- /dev/null +++ b/accounting/storage/ledgercli.py @@ -0,0 +1,324 @@ +import sys +import subprocess +import logging +import time + +from datetime import datetime +from xml.etree import ElementTree +from contextlib import contextmanager + +from accounting.models import Account, Transaction, Posting, Amount +from accounting.storage import Storage + +_log = logging.getLogger(__name__) + + +class Ledger(Storage): + def __init__(self, ledger_file=None, ledger_bin=None): + if ledger_file is None: + raise ValueError('ledger_file cannot be None') + + self.ledger_bin = ledger_bin or 'ledger' + self.ledger_file = ledger_file + _log.info('ledger file: %s', ledger_file) + + self.locked = False + self.ledger_process = None + + @contextmanager + def locked_process(self): + r''' + Context manager that checks that the ledger process is not already + locked, then "locks" the process and yields the process handle and + unlocks the process when execution is returned. + + Since this decorated as a :func:`contextlib.contextmanager` the + recommended use is with the ``with``-statement. + + .. code-block:: python + + with self.locked_process() as p: + p.stdin.write(b'bal\n') + + output = self.read_until_prompt(p) + + ''' + if self.locked: + raise RuntimeError('The process has already been locked,' + ' something\'s out of order.') + + # XXX: This code has no purpose in a single-threaded process + timeout = 5 # Seconds + + for i in range(1, timeout + 2): + if i > timeout: + raise RuntimeError('Ledger process is already locked') + + if not self.locked: + break + else: + _log.info('Waiting for one second... %d/%d', i, timeout) + time.sleep(1) + + process = self.get_process() + + self.locked = True + _log.debug('Lock enabled') + + yield process + + self.locked = False + _log.debug('Lock disabled') + + def assemble_arguments(self): + ''' + Returns a list of arguments suitable for :class:`subprocess.Popen` + based on :attr:`self.ledger_bin` and :attr:`self.ledger_file`. + ''' + return [ + self.ledger_bin, + '-f', + self.ledger_file, + ] + + def init_process(self): + ''' + Creates a new (presumably) ledger subprocess based on the args from + :meth:`Ledger.assemble_arguments()` and then runs + :meth:`Ledger.read_until_prompt()` once (which should return the banner + text) and discards the output. + ''' + _log.debug('Starting ledger process...') + self.ledger_process = subprocess.Popen( + self.assemble_arguments(), + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE) + + # Swallow the banner + with self.locked_process() as p: + self.read_until_prompt(p) + + return self.ledger_process + + def get_process(self): + ''' + Returns :attr:`self.ledger_process` if it evaluates to ``True``. If + :attr:`self.ledger_process` is not set the result of + :meth:`self.init_process() ` is returned. + ''' + return self.ledger_process or self.init_process() + + def read_until_prompt(self, process): + r''' + Reads from the subprocess instance :data:`process` until it finds a + combination of ``\n]\x20`` (the prompt), then returns the output + without the prompt. + ''' + output = b'' + + while True: + line = process.stdout.read(1) # XXX: This is a hack + + output += line + + if b'\n] ' in output: + _log.debug('Found prompt!') + break + + output = output[:-3] # Cut away the prompt + + _log.debug('output: %s', output) + + return output + + def send_command(self, command): + output = None + + with self.locked_process() as p: + if isinstance(command, str): + command = command.encode('utf8') + + p.stdin.write(command + b'\n') + p.stdin.flush() + + output = self.read_until_prompt(p) + + self.ledger_process.send_signal(subprocess.signal.SIGTERM) + _log.debug('Waiting for ledger to shut down') + self.ledger_process.wait() + self.ledger_process = None + + return output + + def add_transaction(self, transaction): + ''' + Writes a transaction to the ledger file by opening it in 'ab' mode and + writing a ledger transaction based on the + :class:`~accounting.models.Transaction` instance in + :data:`transaction`. + ''' + if not transaction.metadata.get('Id'): + transaction.generate_id() + + transaction_template = ('\n{date} {t.payee}\n' + '{tags}' + '{postings}') + + metadata_template = ' ;{0}: {1}\n' + + # TODO: Generate metadata for postings + posting_template = (' {account} {p.amount.symbol}' + ' {p.amount.amount}\n') + + output = b'' + + # XXX: Even I hardly understands what this does, however I indent it it + # stays unreadable. + output += transaction_template.format( + date=transaction.date.strftime('%Y-%m-%d'), + t=transaction, + tags=''.join([ + metadata_template.format(k, v) + for k, v in transaction.metadata.items()]), + postings=''.join([posting_template.format( + p=p, + account=p.account + ' ' * ( + 80 - (len(p.account) + len(p.amount.symbol) + + len(str(p.amount.amount)) + 1 + 2) + )) for p in transaction.postings + ]) + ).encode('utf8') + + with open(self.ledger_file, 'ab') as f: + f.write(output) + + _log.debug('written to file: %s', output) + + def bal(self): + output = self.send_command('xml') + + if output is None: + raise RuntimeError('bal call returned no output') + + accounts = [] + + xml = ElementTree.fromstring(output.decode('utf8')) + + accounts = self._recurse_accounts(xml.find('./accounts')) + + return accounts + + def _recurse_accounts(self, root): + accounts = [] + + for account in root.findall('./account'): + name = account.find('./fullname').text + + amounts = [] + + # Try to find an account total value, then try to find the account + # balance + account_amounts = account.findall( + './account-total/balance/amount') or \ + account.findall('./account-amount/amount') or \ + account.findall('./account-total/amount') + + if account_amounts: + for amount in account_amounts: + quantity = amount.find('./quantity').text + symbol = amount.find('./commodity/symbol').text + + amounts.append(Amount(amount=quantity, symbol=symbol)) + else: + _log.warning('Account %s does not have any amounts', name) + + accounts.append(Account(name=name, + amounts=amounts, + accounts=self._recurse_accounts(account))) + + return accounts + + def reg(self): + output = self.send_command('xml') + + if output is None: + raise RuntimeError('reg call returned no output') + + entries = [] + + reg_xml = ElementTree.fromstring(output.decode('utf8')) + + for transaction in reg_xml.findall('./transactions/transaction'): + date = datetime.strptime(transaction.find('./date').text, + '%Y/%m/%d') + payee = transaction.find('./payee').text + + postings = [] + + for posting in transaction.findall('./postings/posting'): + account = posting.find('./account/name').text + amount = posting.find('./post-amount/amount/quantity').text + symbol = posting.find( + './post-amount/amount/commodity/symbol').text + + # Get the posting metadata + metadata = {} + + values = posting.findall('./metadata/value') + if values: + for value in values: + key = value.get('key') + value = value.find('./string').text + + _log.debug('metadata: %s: %s', key, value) + + metadata.update({key: value}) + + postings.append( + Posting(account=account, + metadata=metadata, + amount=Amount(amount=amount, symbol=symbol))) + + # Get the transaction metadata + metadata = {} + + values = transaction.findall('./metadata/value') + if values: + for value in values: + key = value.get('key') + value = value.find('./string').text + + _log.debug('metadata: %s: %s', key, value) + + metadata.update({key: value}) + + # Add a Transaction instance to the list + id = metadata.pop('Id') + entries.append( + Transaction(id=id, date=date, payee=payee, postings=postings, + metadata=metadata)) + + return entries + + +def main(argv=None): + import argparse + if argv is None: + argv = sys.argv + + parser = argparse.ArgumentParser() + parser.add_argument('-v', '--verbosity', + default='INFO', + help=('Filter logging output. Possible values:' + + ' CRITICAL, ERROR, WARNING, INFO, DEBUG')) + + args = parser.parse_args(argv[1:]) + logging.basicConfig(level=getattr(logging, args.verbosity, 'INFO')) + ledger = Ledger(ledger_file='non-profit-test-data.ledger') + print(ledger.bal()) + print(ledger.reg()) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/accounting/storage/sql/__init__.py b/accounting/storage/sql/__init__.py new file mode 100644 index 0000000..2145134 --- /dev/null +++ b/accounting/storage/sql/__init__.py @@ -0,0 +1,72 @@ +import logging +import json + +from flask.ext.sqlalchemy import SQLAlchemy + +from accounting.storage import Storage +from accounting.models import Transaction, Posting, Amount + +_log = logging.getLogger(__name__) +db = None + + +class SQLStorage(Storage): + def __init__(self, app): + global db + self.app = app + db = self.db = SQLAlchemy(app) + + from .models import Transaction as SQLTransaction, \ + Posting as SQLPosting, Amount as SQLAmount + + db.create_all() + + self.Transaction = SQLTransaction + self.Posting = SQLPosting + self.Amount = SQLAmount + + def get_transactions(self, *args, **kw): + transactions = [] + + for transaction in self.Transaction.query.all(): + dict_transaction = transaction.as_dict() + dict_postings = dict_transaction.pop('postings') + + postings = [] + + for dict_posting in dict_postings: + dict_amount = dict_posting.pop('amount') + posting = Posting(**dict_posting) + posting.amount = Amount(**dict_amount) + + postings.append(posting) + + dict_transaction.update({'postings': postings}) + + transactions.append(Transaction(**dict_transaction)) + + return transactions + + def add_transaction(self, transaction): + if transaction.id is None: + transaction.generate_id() + + _t = self.Transaction() + _t.uuid = str(transaction.id) + _t.date = transaction.date + _t.payee = transaction.payee + _t.meta = json.dumps(transaction.metadata) + + self.db.session.add(_t) + + for posting in transaction.postings: + _p = self.Posting() + _p.transaction_uuid = str(transaction.id) + _p.account = posting.account + _p.meta = json.dumps(posting.metadata) + _p.amount = self.Amount(symbol=posting.amount.symbol, + amount=posting.amount.amount) + + self.db.session.add(_p) + + self.db.session.commit() diff --git a/accounting/storage/sql/models.py b/accounting/storage/sql/models.py new file mode 100644 index 0000000..72dfab9 --- /dev/null +++ b/accounting/storage/sql/models.py @@ -0,0 +1,53 @@ +import json + +from . import db + + +class Transaction(db.Model): + id = db.Column(db.Integer(), primary_key=True) + uuid = db.Column(db.String, unique=True, nullable=False) + date = db.Column(db.DateTime) + payee = db.Column(db.String()) + meta = db.Column(db.String()) + + def as_dict(self): + return dict( + id=self.uuid, + date=self.date, + payee=self.payee, + postings=[p.as_dict() for p in self.postings], + metadata=json.loads(self.meta) + ) + + +class Posting(db.Model): + id = db.Column(db.Integer(), primary_key=True) + + transaction_uuid = db.Column(db.String, db.ForeignKey('transaction.uuid')) + transaction = db.relationship('Transaction', backref='postings') + + account = db.Column(db.String, nullable=False) + + amount_id = db.Column(db.Integer, db.ForeignKey('amount.id')) + amount = db.relationship('Amount') + + meta = db.Column(db.String) + + def as_dict(self): + return dict( + account=self.account, + amount=self.amount.as_dict(), + metadata=json.loads(self.meta) + ) + + +class Amount(db.Model): + id = db.Column(db.Integer, primary_key=True) + symbol = db.Column(db.String) + amount = db.Column(db.Numeric) + + def as_dict(self): + return dict( + symbol=self.symbol, + amount=self.amount + ) diff --git a/accounting/web.py b/accounting/web.py index abdc861..8140ad1 100644 --- a/accounting/web.py +++ b/accounting/web.py @@ -7,8 +7,12 @@ import logging import argparse from flask import Flask, jsonify, request +from flask.ext.sqlalchemy import SQLAlchemy +from flask.ext.script import Manager +from flask.ext.migrate import Migrate, MigrateCommand -from accounting import Ledger +from accounting.storage.ledgercli import Ledger +from accounting.storage.sql import SQLStorage from accounting.transport import AccountingEncoder, AccountingDecoder from accounting.exceptions import AccountingException from accounting.decorators import jsonify_exceptions @@ -17,7 +21,15 @@ from accounting.decorators import jsonify_exceptions app = Flask('accounting') app.config.from_pyfile('config.py') -ledger = None +storage = SQLStorage(app) + +# TODO: Move migration stuff into SQLStorage +db = storage.db +migrate = Migrate(app, db) + +manager = Manager(app) +manager.add_command('db', MigrateCommand) + @app.before_request def init_ledger(): @@ -26,7 +38,7 @@ def init_ledger(): :py:class:`accounting.Ledger` object. ''' global ledger - ledger = Ledger(ledger_file=app.config['LEDGER_FILE']) + #ledger = Ledger(ledger_file=app.config['LEDGER_FILE']) # These will convert output from our internal classes to JSON and back @@ -40,21 +52,13 @@ def index(): return 'Hello World!' -@app.route('/balance') -def balance_report(): - ''' - Returns the JSON-serialized result of :meth:`accounting.Ledger.bal` - ''' - report_data = ledger.bal() - - return jsonify(balance_report=report_data) - @app.route('/transaction', methods=['GET']) def transaction_get(): ''' Returns the JSON-serialized output of :meth:`accounting.Ledger.reg` ''' - return jsonify(transactions=ledger.reg()) + return jsonify(transactions=storage.get_transactions()) + @app.route('/transaction', methods=['POST']) @jsonify_exceptions @@ -112,7 +116,7 @@ def transaction_post(): raise AccountingException('No transaction data provided') for transaction in transactions: - ledger.add_transaction(transaction) + storage.add_transaction(transaction) return jsonify(foo='bar') @@ -166,16 +170,6 @@ def parse_json(): return jsonify(foo='bar') -@app.route('/register') -def register_report(): - ''' - Returns the JSON-serialized output of :py:meth:`accounting.Ledger.reg` - ''' - report_data = ledger.reg() - - return jsonify(register_report=report_data) - - def main(argv=None): prog = __name__ if argv is None: @@ -186,7 +180,7 @@ def main(argv=None): parser.add_argument('-v', '--verbosity', default='INFO', help=('Filter logging output. Possible values:' + - ' CRITICAL, ERROR, WARNING, INFO, DEBUG')) + ' CRITICAL, ERROR, WARNING, INFO, DEBUG')) args = parser.parse_args(argv)