[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.
This commit is contained in:
parent
36d91dd0b3
commit
281d6fed47
7 changed files with 70 additions and 41 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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))
|
||||
|
|
|
@ -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__')
|
||||
|
||||
|
|
|
@ -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/<string:transaction_id>', 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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue