From 281d6fed47b7b703f8fe27f35c0b85fa051f0ceb Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 21 Dec 2013 00:24:37 +0100 Subject: [PATCH] [test] Added unit tests - Moved the TransactionNotFound exception to a more appropriate place. - Changed the serialization for AccountingExceptions - Override the Exception.__init__ method in AccountingException - Added __eq__ methods to accounting.models.* - Catch the TransactionNotFound exception in transaction_get and return a 404 instead. This could be improved, perhaps in the jsonify_exceptions decorator so that all endpoints that raise a TransactionNotFound exception automatically return a 404. --- accounting/exceptions.py | 7 ++++++ accounting/models.py | 18 +++++++++++++++- accounting/storage/__init__.py | 6 ------ accounting/storage/ledgercli.py | 22 ++++++++++++++----- accounting/transport.py | 9 ++++---- accounting/web.py | 38 +++++++++++---------------------- non-profit-test-data.ledger | 11 ++++++++++ 7 files changed, 70 insertions(+), 41 deletions(-) diff --git a/accounting/exceptions.py b/accounting/exceptions.py index a2e841e..a95cc42 100644 --- a/accounting/exceptions.py +++ b/accounting/exceptions.py @@ -7,4 +7,11 @@ class AccountingException(Exception): Used as a base for exceptions that are returned to the caller via the jsonify_exceptions decorator ''' + def __init__(self, message, **kw): + self.message = message + for key, value in kw.items(): + setattr(self, key, value) + + +class TransactionNotFound(AccountingException): pass diff --git a/accounting/models.py b/accounting/models.py index 3b48e37..bb7390d 100644 --- a/accounting/models.py +++ b/accounting/models.py @@ -1,14 +1,18 @@ # Part of accounting-api project: # https://gitorious.org/conservancy/accounting-api # License: AGPLv3-or-later - +import datetime import uuid + from decimal import Decimal class Transaction: def __init__(self, id=None, date=None, payee=None, postings=None, metadata=None, _generate_id=False): + if type(date) == datetime.datetime: + date = date.date() + self.id = id self.date = date self.payee = payee @@ -21,6 +25,9 @@ class Transaction: def generate_id(self): self.id = str(uuid.uuid4()) + def __eq__(self, other): + return self.__dict__ == other.__dict__ + def __repr__(self): return ('<{self.__class__.__name__} {self.id} {date}' + ' {self.payee} {self.postings}').format( @@ -34,6 +41,9 @@ class Posting: self.amount = amount self.metadata = metadata if metadata is not None else {} + def __eq__(self, other): + return self.__dict__ == other.__dict__ + def __repr__(self): return ('<{self.__class__.__name__} "{self.account}"' + ' {self.amount}>').format(self=self) @@ -44,6 +54,9 @@ class Amount: self.amount = Decimal(amount) self.symbol = symbol + def __eq__(self, other): + return self.__dict__ == other.__dict__ + def __repr__(self): return ('<{self.__class__.__name__} {self.symbol}' + ' {self.amount}>').format(self=self) @@ -55,6 +68,9 @@ class Account: self.amounts = amounts self.accounts = accounts + def __eq__(self, other): + return self.__dict__ == other.__dict__ + def __repr__(self): return ('<{self.__class__.__name__} "{self.name}" {self.amounts}' + ' {self.accounts}>').format(self=self) diff --git a/accounting/storage/__init__.py b/accounting/storage/__init__.py index 543607a..8194a05 100644 --- a/accounting/storage/__init__.py +++ b/accounting/storage/__init__.py @@ -4,8 +4,6 @@ from abc import ABCMeta, abstractmethod -from accounting.exceptions import AccountingException - class Storage: ''' @@ -47,7 +45,3 @@ class Storage: @abstractmethod def reverse_transaction(self, transaction_id): raise NotImplementedError - - -class TransactionNotFound(AccountingException): - pass diff --git a/accounting/storage/ledgercli.py b/accounting/storage/ledgercli.py index d47064d..31cf91f 100644 --- a/accounting/storage/ledgercli.py +++ b/accounting/storage/ledgercli.py @@ -12,9 +12,9 @@ from datetime import datetime from xml.etree import ElementTree from contextlib import contextmanager -from accounting.exceptions import AccountingException +from accounting.exceptions import AccountingException, TransactionNotFound from accounting.models import Account, Transaction, Posting, Amount -from accounting.storage import Storage, TransactionNotFound +from accounting.storage import Storage _log = logging.getLogger(__name__) @@ -129,6 +129,10 @@ class Ledger(Storage): while True: line = process.stdout.read(1) # XXX: This is a hack + if len(line) > 0: + pass + #_log.debug('line: %s', line) + output += line if b'\n] ' in output: @@ -148,6 +152,8 @@ class Ledger(Storage): if isinstance(command, str): command = command.encode('utf8') + _log.debug('Sending command: %r', command) + p.stdin.write(command + b'\n') p.stdin.flush() @@ -263,8 +269,8 @@ class Ledger(Storage): if transaction.id == transaction_id: return transaction - raise TransactionNotFound('No transaction with id %s found', - transaction_id) + raise TransactionNotFound( + 'No transaction with id {0} found'.format(transaction_id)) def reg(self): output = self.send_command('xml') @@ -321,7 +327,13 @@ class Ledger(Storage): metadata.update({key: value}) # Add a Transaction instance to the list - id = metadata.pop('Id') + try: + id = metadata.pop('Id') + except KeyError: + _log.warning('Transaction on %s with payee %s does not have an' + ' Id attribute. A temporary ID will be used.', + date, payee) + id = 'NO-ID' entries.append( Transaction(id=id, date=date, payee=payee, postings=postings, metadata=metadata)) diff --git a/accounting/transport.py b/accounting/transport.py index 2e4a494..e838be9 100644 --- a/accounting/transport.py +++ b/accounting/transport.py @@ -6,6 +6,7 @@ from datetime import datetime from flask import json +from accounting.exceptions import AccountingException from accounting.models import Amount, Transaction, Posting, Account @@ -40,10 +41,10 @@ class AccountingEncoder(json.JSONEncoder): amount=str(o.amount), symbol=o.symbol ) - elif isinstance(o, Exception): + elif isinstance(o, AccountingException): return dict( - __type__=o.__class__.__name__, - args=o.args + type=o.__class__.__name__, + message=o.message ) return json.JSONEncoder.default(self, o) @@ -58,7 +59,7 @@ class AccountingDecoder(json.JSONDecoder): return d types = {c.__name__: c for c in [Amount, Transaction, Posting, - Account]} + Account, AccountingException]} _type = d.pop('__type__') diff --git a/accounting/web.py b/accounting/web.py index ea6ae51..546d955 100644 --- a/accounting/web.py +++ b/accounting/web.py @@ -10,7 +10,7 @@ import sys import logging import argparse -from flask import Flask, jsonify, request, render_template +from flask import Flask, jsonify, request, render_template, abort from flask.ext.script import Manager from flask.ext.migrate import Migrate, MigrateCommand @@ -19,32 +19,18 @@ from accounting.storage import Storage 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.exceptions import AccountingException, TransactionNotFound from accounting.decorators import jsonify_exceptions, cors app = Flask('accounting') app.config.from_pyfile('config.py') -storage = Storage() - -if isinstance(storage, SQLStorage): - # TODO: Move migration stuff into SQLStorage - db = storage.db - migrate = Migrate(app, db) - - manager = Manager(app) - manager.add_command('db', MigrateCommand) +app.ledger = Storage() -@app.before_request def init_ledger(): - ''' - :py:meth:`flask.Flask.before_request`-decorated method that initializes an - :py:class:`accounting.Ledger` object. - ''' - global ledger - #ledger = Ledger(ledger_file=app.config['LEDGER_FILE']) + app.ledger = Ledger(app) # These will convert output from our internal classes to JSON and back @@ -85,9 +71,12 @@ def transaction_get(transaction_id=None): Returns the JSON-serialized output of :meth:`accounting.Ledger.reg` ''' if transaction_id is None: - return jsonify(transactions=storage.get_transactions()) + return jsonify(transactions=app.ledger.get_transactions()) - return jsonify(transaction=storage.get_transaction(transaction_id)) + try: + return jsonify(transaction=app.ledger.get_transaction(transaction_id)) + except TransactionNotFound: + abort(404) @app.route('/transaction/', methods=['POST']) @@ -105,7 +94,7 @@ def transaction_update(transaction_id=None): elif transaction.id is None: transaction.id = transaction_id - storage.update_transaction(transaction) + app.ledger.update_transaction(transaction) return jsonify(status='OK') @@ -117,7 +106,7 @@ def transaction_delete(transaction_id=None): if transaction_id is None: raise AccountingException('Transaction ID cannot be None') - storage.delete_transaction(transaction_id) + app.ledger.delete_transaction(transaction_id) return jsonify(status='OK') @@ -184,7 +173,7 @@ def transaction_post(): transaction_ids = [] for transaction in transactions: - transaction_ids.append(storage.add_transaction(transaction)) + transaction_ids.append(app.ledger.add_transaction(transaction)) return jsonify(status='OK', transaction_ids=transaction_ids) @@ -201,8 +190,7 @@ def main(argv=None): help=('Filter logging output. Possible values:' + ' CRITICAL, ERROR, WARNING, INFO, DEBUG')) - global storage - storage = Ledger(app=app) + init_ledger() args = parser.parse_args(argv) diff --git a/non-profit-test-data.ledger b/non-profit-test-data.ledger index ebf48a1..d4ece1a 100644 --- a/non-profit-test-data.ledger +++ b/non-profit-test-data.ledger @@ -23,6 +23,7 @@ Assets:Checking $50.00 2011/04/20 (2) Baz Hosting Services, LLC + ;Party: Baz Hosting Services, LLC ;Id: this is probably unique Expenses:Blah:Hosting $250.00 ;Receipt: Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf @@ -44,3 +45,13 @@ ;Id: 31048b9d-a5b6-41d7-951a-e7128e7c53c0 Income:Donations:PayPal $ -20.18 Assets:Checking $ 20.18 + +2013-12-19 AngularJS Donation + ;Id: 593d921c-07f9-496d-a1b7-47bb68ff00d4 + Assets:Checking USD -10.00 + Expenses:Donations USD 10.00 + +2013-12-20 PageKite donation + ;Id: b3576a94-2a53-4d40-bd92-06b2532937f5 + Assets:Checking USD -10.00 + Expenses:Donations USD 10.00