[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 | ||||
|             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)) | ||||
|  |  | |||
|  | @ -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…
	
	Add table
		
		Reference in a new issue
	
	 Joar Wandborg
						Joar Wandborg