[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 |     Used as a base for exceptions that are returned to the caller via the | ||||||
|     jsonify_exceptions decorator |     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 |     pass | ||||||
|  |  | ||||||
|  | @ -1,14 +1,18 @@ | ||||||
| # Part of accounting-api project: | # Part of accounting-api project: | ||||||
| # https://gitorious.org/conservancy/accounting-api | # https://gitorious.org/conservancy/accounting-api | ||||||
| # License: AGPLv3-or-later | # License: AGPLv3-or-later | ||||||
| 
 | import datetime | ||||||
| import uuid | import uuid | ||||||
|  | 
 | ||||||
| from decimal import Decimal | from decimal import Decimal | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Transaction: | class Transaction: | ||||||
|     def __init__(self, id=None, date=None, payee=None, postings=None, |     def __init__(self, id=None, date=None, payee=None, postings=None, | ||||||
|                  metadata=None, _generate_id=False): |                  metadata=None, _generate_id=False): | ||||||
|  |         if type(date) == datetime.datetime: | ||||||
|  |             date = date.date() | ||||||
|  | 
 | ||||||
|         self.id = id |         self.id = id | ||||||
|         self.date = date |         self.date = date | ||||||
|         self.payee = payee |         self.payee = payee | ||||||
|  | @ -21,6 +25,9 @@ class Transaction: | ||||||
|     def generate_id(self): |     def generate_id(self): | ||||||
|         self.id = str(uuid.uuid4()) |         self.id = str(uuid.uuid4()) | ||||||
| 
 | 
 | ||||||
|  |     def __eq__(self, other): | ||||||
|  |         return self.__dict__ == other.__dict__ | ||||||
|  | 
 | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return ('<{self.__class__.__name__} {self.id} {date}' + |         return ('<{self.__class__.__name__} {self.id} {date}' + | ||||||
|                 ' {self.payee} {self.postings}').format( |                 ' {self.payee} {self.postings}').format( | ||||||
|  | @ -34,6 +41,9 @@ class Posting: | ||||||
|         self.amount = amount |         self.amount = amount | ||||||
|         self.metadata = metadata if metadata is not None else {} |         self.metadata = metadata if metadata is not None else {} | ||||||
| 
 | 
 | ||||||
|  |     def __eq__(self, other): | ||||||
|  |         return self.__dict__ == other.__dict__ | ||||||
|  | 
 | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return ('<{self.__class__.__name__} "{self.account}"' + |         return ('<{self.__class__.__name__} "{self.account}"' + | ||||||
|                 ' {self.amount}>').format(self=self) |                 ' {self.amount}>').format(self=self) | ||||||
|  | @ -44,6 +54,9 @@ class Amount: | ||||||
|         self.amount = Decimal(amount) |         self.amount = Decimal(amount) | ||||||
|         self.symbol = symbol |         self.symbol = symbol | ||||||
| 
 | 
 | ||||||
|  |     def __eq__(self, other): | ||||||
|  |         return self.__dict__ == other.__dict__ | ||||||
|  | 
 | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return ('<{self.__class__.__name__} {self.symbol}' + |         return ('<{self.__class__.__name__} {self.symbol}' + | ||||||
|                 ' {self.amount}>').format(self=self) |                 ' {self.amount}>').format(self=self) | ||||||
|  | @ -55,6 +68,9 @@ class Account: | ||||||
|         self.amounts = amounts |         self.amounts = amounts | ||||||
|         self.accounts = accounts |         self.accounts = accounts | ||||||
| 
 | 
 | ||||||
|  |     def __eq__(self, other): | ||||||
|  |         return self.__dict__ == other.__dict__ | ||||||
|  | 
 | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return ('<{self.__class__.__name__} "{self.name}" {self.amounts}' + |         return ('<{self.__class__.__name__} "{self.name}" {self.amounts}' + | ||||||
|                 ' {self.accounts}>').format(self=self) |                 ' {self.accounts}>').format(self=self) | ||||||
|  |  | ||||||
|  | @ -4,8 +4,6 @@ | ||||||
| 
 | 
 | ||||||
| from abc import ABCMeta, abstractmethod | from abc import ABCMeta, abstractmethod | ||||||
| 
 | 
 | ||||||
| from accounting.exceptions import AccountingException |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| class Storage: | class Storage: | ||||||
|     ''' |     ''' | ||||||
|  | @ -47,7 +45,3 @@ class Storage: | ||||||
|     @abstractmethod |     @abstractmethod | ||||||
|     def reverse_transaction(self, transaction_id): |     def reverse_transaction(self, transaction_id): | ||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class TransactionNotFound(AccountingException): |  | ||||||
|     pass |  | ||||||
|  |  | ||||||
|  | @ -12,9 +12,9 @@ from datetime import datetime | ||||||
| from xml.etree import ElementTree | from xml.etree import ElementTree | ||||||
| from contextlib import contextmanager | 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.models import Account, Transaction, Posting, Amount | ||||||
| from accounting.storage import Storage, TransactionNotFound | from accounting.storage import Storage | ||||||
| 
 | 
 | ||||||
| _log = logging.getLogger(__name__) | _log = logging.getLogger(__name__) | ||||||
| 
 | 
 | ||||||
|  | @ -129,6 +129,10 @@ class Ledger(Storage): | ||||||
|         while True: |         while True: | ||||||
|             line = process.stdout.read(1)  # XXX: This is a hack |             line = process.stdout.read(1)  # XXX: This is a hack | ||||||
| 
 | 
 | ||||||
|  |             if len(line) > 0: | ||||||
|  |                 pass | ||||||
|  |                 #_log.debug('line: %s', line) | ||||||
|  | 
 | ||||||
|             output += line |             output += line | ||||||
| 
 | 
 | ||||||
|             if b'\n] ' in output: |             if b'\n] ' in output: | ||||||
|  | @ -148,6 +152,8 @@ class Ledger(Storage): | ||||||
|             if isinstance(command, str): |             if isinstance(command, str): | ||||||
|                 command = command.encode('utf8') |                 command = command.encode('utf8') | ||||||
| 
 | 
 | ||||||
|  |             _log.debug('Sending command: %r', command) | ||||||
|  | 
 | ||||||
|             p.stdin.write(command + b'\n') |             p.stdin.write(command + b'\n') | ||||||
|             p.stdin.flush() |             p.stdin.flush() | ||||||
| 
 | 
 | ||||||
|  | @ -263,8 +269,8 @@ class Ledger(Storage): | ||||||
|             if transaction.id == transaction_id: |             if transaction.id == transaction_id: | ||||||
|                 return transaction |                 return transaction | ||||||
| 
 | 
 | ||||||
|         raise TransactionNotFound('No transaction with id %s found', |         raise TransactionNotFound( | ||||||
|                                   transaction_id) |             'No transaction with id {0} found'.format(transaction_id)) | ||||||
| 
 | 
 | ||||||
|     def reg(self): |     def reg(self): | ||||||
|         output = self.send_command('xml') |         output = self.send_command('xml') | ||||||
|  | @ -321,7 +327,13 @@ class Ledger(Storage): | ||||||
|                     metadata.update({key: value}) |                     metadata.update({key: value}) | ||||||
| 
 | 
 | ||||||
|             # Add a Transaction instance to the list |             # 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( |             entries.append( | ||||||
|                 Transaction(id=id, date=date, payee=payee, postings=postings, |                 Transaction(id=id, date=date, payee=payee, postings=postings, | ||||||
|                             metadata=metadata)) |                             metadata=metadata)) | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ from datetime import datetime | ||||||
| 
 | 
 | ||||||
| from flask import json | from flask import json | ||||||
| 
 | 
 | ||||||
|  | from accounting.exceptions import AccountingException | ||||||
| from accounting.models import Amount, Transaction, Posting, Account | from accounting.models import Amount, Transaction, Posting, Account | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -40,10 +41,10 @@ class AccountingEncoder(json.JSONEncoder): | ||||||
|                 amount=str(o.amount), |                 amount=str(o.amount), | ||||||
|                 symbol=o.symbol |                 symbol=o.symbol | ||||||
|             ) |             ) | ||||||
|         elif isinstance(o, Exception): |         elif isinstance(o, AccountingException): | ||||||
|             return dict( |             return dict( | ||||||
|                 __type__=o.__class__.__name__, |                 type=o.__class__.__name__, | ||||||
|                 args=o.args |                 message=o.message | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         return json.JSONEncoder.default(self, o) |         return json.JSONEncoder.default(self, o) | ||||||
|  | @ -58,7 +59,7 @@ class AccountingDecoder(json.JSONDecoder): | ||||||
|             return d |             return d | ||||||
| 
 | 
 | ||||||
|         types = {c.__name__: c for c in [Amount, Transaction, Posting, |         types = {c.__name__: c for c in [Amount, Transaction, Posting, | ||||||
|                                          Account]} |                                          Account, AccountingException]} | ||||||
| 
 | 
 | ||||||
|         _type = d.pop('__type__') |         _type = d.pop('__type__') | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ import sys | ||||||
| import logging | import logging | ||||||
| import argparse | 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.script import Manager | ||||||
| from flask.ext.migrate import Migrate, MigrateCommand | 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.ledgercli import Ledger | ||||||
| from accounting.storage.sql import SQLStorage | from accounting.storage.sql import SQLStorage | ||||||
| from accounting.transport import AccountingEncoder, AccountingDecoder | from accounting.transport import AccountingEncoder, AccountingDecoder | ||||||
| from accounting.exceptions import AccountingException | from accounting.exceptions import AccountingException, TransactionNotFound | ||||||
| from accounting.decorators import jsonify_exceptions, cors | from accounting.decorators import jsonify_exceptions, cors | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| app = Flask('accounting') | app = Flask('accounting') | ||||||
| app.config.from_pyfile('config.py') | app.config.from_pyfile('config.py') | ||||||
| 
 | 
 | ||||||
| storage = Storage() | app.ledger = 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.before_request |  | ||||||
| def init_ledger(): | def init_ledger(): | ||||||
|     ''' |     app.ledger = Ledger(app) | ||||||
|     :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']) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # These will convert output from our internal classes to JSON and back | # 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` |     Returns the JSON-serialized output of :meth:`accounting.Ledger.reg` | ||||||
|     ''' |     ''' | ||||||
|     if transaction_id is None: |     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']) | @app.route('/transaction/<string:transaction_id>', methods=['POST']) | ||||||
|  | @ -105,7 +94,7 @@ def transaction_update(transaction_id=None): | ||||||
|     elif transaction.id is None: |     elif transaction.id is None: | ||||||
|         transaction.id = transaction_id |         transaction.id = transaction_id | ||||||
| 
 | 
 | ||||||
|     storage.update_transaction(transaction) |     app.ledger.update_transaction(transaction) | ||||||
| 
 | 
 | ||||||
|     return jsonify(status='OK') |     return jsonify(status='OK') | ||||||
| 
 | 
 | ||||||
|  | @ -117,7 +106,7 @@ def transaction_delete(transaction_id=None): | ||||||
|     if transaction_id is None: |     if transaction_id is None: | ||||||
|         raise AccountingException('Transaction ID cannot be None') |         raise AccountingException('Transaction ID cannot be None') | ||||||
| 
 | 
 | ||||||
|     storage.delete_transaction(transaction_id) |     app.ledger.delete_transaction(transaction_id) | ||||||
| 
 | 
 | ||||||
|     return jsonify(status='OK') |     return jsonify(status='OK') | ||||||
| 
 | 
 | ||||||
|  | @ -184,7 +173,7 @@ def transaction_post(): | ||||||
|     transaction_ids = [] |     transaction_ids = [] | ||||||
| 
 | 
 | ||||||
|     for transaction in transactions: |     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) |     return jsonify(status='OK', transaction_ids=transaction_ids) | ||||||
| 
 | 
 | ||||||
|  | @ -201,8 +190,7 @@ def main(argv=None): | ||||||
|                         help=('Filter logging output. Possible values:' + |                         help=('Filter logging output. Possible values:' + | ||||||
|                               ' CRITICAL, ERROR, WARNING, INFO, DEBUG')) |                               ' CRITICAL, ERROR, WARNING, INFO, DEBUG')) | ||||||
| 
 | 
 | ||||||
|     global storage |     init_ledger() | ||||||
|     storage = Ledger(app=app) |  | ||||||
| 
 | 
 | ||||||
|     args = parser.parse_args(argv) |     args = parser.parse_args(argv) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ | ||||||
|    Assets:Checking   $50.00 |    Assets:Checking   $50.00 | ||||||
| 
 | 
 | ||||||
| 2011/04/20 (2) Baz Hosting Services, LLC | 2011/04/20 (2) Baz Hosting Services, LLC | ||||||
|  |     ;Party: Baz Hosting Services, LLC | ||||||
|     ;Id: this is probably unique |     ;Id: this is probably unique | ||||||
|    Expenses:Blah:Hosting   $250.00 |    Expenses:Blah:Hosting   $250.00 | ||||||
|       ;Receipt: Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf |       ;Receipt: Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf | ||||||
|  | @ -44,3 +45,13 @@ | ||||||
|    ;Id: 31048b9d-a5b6-41d7-951a-e7128e7c53c0 |    ;Id: 31048b9d-a5b6-41d7-951a-e7128e7c53c0 | ||||||
|   Income:Donations:PayPal                                                $ -20.18 |   Income:Donations:PayPal                                                $ -20.18 | ||||||
|   Assets:Checking                                                         $ 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…
	
	Add table
		
		Reference in a new issue
	
	 Joar Wandborg
						Joar Wandborg