From 02fc05aebddef960e59bc9be17cdaad25ace1f9a Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Wed, 18 Dec 2013 22:02:03 +0100 Subject: [PATCH] [storage] Added delete_transaction method - Added storage.ledgercli implementation - Added storage.ledgercli update_transaction - Added storage.ledgercli get_transaction - Pushing pre-built docs --- accounting/storage/__init__.py | 12 +- accounting/storage/ledgercli.py | 121 ++++++++++++++++++++- accounting/web.py | 25 ++++- doc/build/html/api/accounting.html | 5 + doc/build/html/api/accounting.storage.html | 26 ++++- doc/build/html/genindex.html | 14 ++- doc/build/html/objects.inv | Bin 1127 -> 1119 bytes doc/build/html/searchindex.js | 2 +- 8 files changed, 191 insertions(+), 14 deletions(-) diff --git a/accounting/storage/__init__.py b/accounting/storage/__init__.py index 28922c8..543607a 100644 --- a/accounting/storage/__init__.py +++ b/accounting/storage/__init__.py @@ -4,8 +4,10 @@ from abc import ABCMeta, abstractmethod +from accounting.exceptions import AccountingException -class Storage(): + +class Storage: ''' ABC for accounting storage ''' @@ -38,6 +40,14 @@ class Storage(): def update_transaction(self, transaction): raise NotImplementedError + @abstractmethod + def delete_transaction(self, transaction_id): + raise NotImplementedError + @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 bb7f3fa..9c2691f 100644 --- a/accounting/storage/ledgercli.py +++ b/accounting/storage/ledgercli.py @@ -6,13 +6,15 @@ import sys import subprocess import logging import time +import re from datetime import datetime from xml.etree import ElementTree from contextlib import contextmanager +from accounting.exceptions import AccountingException from accounting.models import Account, Transaction, Posting, Amount -from accounting.storage import Storage +from accounting.storage import Storage, TransactionNotFound _log = logging.getLogger(__name__) @@ -205,6 +207,8 @@ class Ledger(Storage): _log.debug('written to file: %s', output) + return transaction.id + def bal(self): output = self.send_command('xml') @@ -252,6 +256,16 @@ class Ledger(Storage): def get_transactions(self): return self.reg() + def get_transaction(self, transaction_id): + transactions = self.get_transactions() + + for transaction in transactions: + if transaction.id == transaction_id: + return transaction + + raise TransactionNotFound('No transaction with id %s found', + transaction_id) + def reg(self): output = self.send_command('xml') @@ -314,8 +328,111 @@ class Ledger(Storage): return entries + def delete_transaction(self, transaction_id): + ''' + Delete a transaction from the ledger file. + + This method opens the ledger file, loads all lines into memory and + looks for the transaction_id, then looks for the bounds of that + transaction in the ledger file, removes all lines within the bounds of + the transaction and removes them, then writes the lines back to the + ledger file. + + Exceptions: + + - RuntimeError: If all boundaries to the transaction are not found + - TransactionNotFound: If no such transaction_id can be found in + :data:`self.ledger_file` + ''' + f = open(self.ledger_file, 'r') + + lines = [i for i in f] + + # A mapping of line meanings and their line numbers as found by the + # following logic + semantic_lines = dict( + id_location=None, + transaction_start=None, + next_transaction_or_eof=None + ) + + for i, line in enumerate(lines): + if transaction_id in line: + semantic_lines['id_location'] = i + break + + if not semantic_lines['id_location']: + raise TransactionNotFound('No transaction with ID "%s" found') + + transaction_start_pattern = re.compile(r'^\S') + + cursor = semantic_lines['id_location'] - 1 + + # Find the first line of the transaction + while True: + if transaction_start_pattern.match(lines[cursor]): + semantic_lines['transaction_start'] = cursor + break + + cursor -= 1 + + cursor = semantic_lines['id_location'] + 1 + + # Find the last line of the transaction + while True: + try: + if transaction_start_pattern.match(lines[cursor]): + semantic_lines['next_transaction_or_eof'] = cursor + break + except IndexError: + # Set next_line_without_starting_space_or_end_of_file to + # the cursor. The cursor will be an index not included in the + # list of lines + semantic_lines['next_transaction_or_eof'] = cursor + break + + cursor += 1 + + if not all(map(lambda v: v is not None, semantic_lines.values())): + raise RuntimeError('Could not find all the values necessary for' + ' safe deletion of a transaction.') + + del_start = semantic_lines['transaction_start'] + + if len(lines) == semantic_lines['next_transaction_or_eof']: + _log.debug('There are no transactions below the transaction being' + ' deleted. The line before the first line of the' + ' transaction will be deleted.') + # Delete the preceding line to make the file + del_start -= 1 + + del lines[del_start:semantic_lines['next_transaction_or_eof']] + + with open(self.ledger_file, 'w') as f: + for line in lines: + f.write(line) + def update_transaction(self, transaction): - _log.debug('DUMMY: Updated transaction: %s', transaction) + ''' + Update a transaction in the ledger file. + + Takes a :class:`~accounting.models.Transaction` object and removes + the old transaction using :data:`transaction.id` from the passed + :class:`~accounting.models.Transaction` instance and adds + :data:`transaction` to the database. + ''' + if not transaction.id: + return AccountingException('The transaction %s has no' + ' id attribute', transaction) + + old_transaction = self.get_transaction(transaction.id) + + self.delete_transaction(transaction.id) + + self.add_transaction(transaction) + + _log.debug('Updated transaction from: %s to: %s', old_transaction, + transaction) def main(argv=None): diff --git a/accounting/web.py b/accounting/web.py index 60564e0..53d77ce 100644 --- a/accounting/web.py +++ b/accounting/web.py @@ -11,7 +11,6 @@ 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 @@ -65,6 +64,7 @@ def transaction_get(): ''' return jsonify(transactions=storage.get_transactions()) + @app.route('/transaction/', methods=['POST']) @jsonify_exceptions def transaction_update(transaction_id=None): @@ -74,8 +74,8 @@ def transaction_update(transaction_id=None): transaction = request.json['transaction'] if transaction.id is not None and not transaction.id == transaction_id: - raise AccountingException('The transaction data has an ID attribute and' - ' it is not the same ID as in the path') + raise AccountingException('The transaction data has an ID attribute' + ' and it is not the same ID as in the path') elif transaction.id is None: transaction.id = transaction_id @@ -84,6 +84,17 @@ def transaction_update(transaction_id=None): return jsonify(status='OK') +@app.route('/transaction/', methods=['DELETE']) +@jsonify_exceptions +def transaction_delete(transaction_id=None): + if transaction_id is None: + raise AccountingException('Transaction ID cannot be None') + + storage.delete_transaction(transaction_id) + + return jsonify(status='OK') + + @app.route('/transaction', methods=['POST']) @jsonify_exceptions def transaction_post(): @@ -139,10 +150,12 @@ def transaction_post(): if not transactions: raise AccountingException('No transaction data provided') - for transaction in transactions: - storage.add_transaction(transaction) + transaction_ids = [] - return jsonify(status='OK') + for transaction in transactions: + transaction_ids.append(storage.add_transaction(transaction)) + + return jsonify(status='OK', transaction_ids=transaction_ids) def main(argv=None): diff --git a/doc/build/html/api/accounting.html b/doc/build/html/api/accounting.html index f2ac9a6..583bde1 100644 --- a/doc/build/html/api/accounting.html +++ b/doc/build/html/api/accounting.html @@ -285,6 +285,11 @@ and the Flask endpoints.

accounting.web.main(argv=None)[source]
+
+
+accounting.web.transaction_delete(transaction_id=None)
+
+
accounting.web.transaction_get()[source]
diff --git a/doc/build/html/api/accounting.storage.html b/doc/build/html/api/accounting.storage.html index c093312..8bef1d0 100644 --- a/doc/build/html/api/accounting.storage.html +++ b/doc/build/html/api/accounting.storage.html @@ -102,7 +102,19 @@ based on self.led
delete_transaction(transaction_id)
-
+

Delete a transaction from the ledger file.

+

This method opens the ledger file, loads all lines into memory and +looks for the transaction_id, then looks for the bounds of that +transaction in the ledger file, removes all lines within the bounds of +the transaction and removes them, then writes the lines back to the +ledger file.

+

Exceptions:

+
    +
  • RuntimeError: If all boundaries to the transaction are not found
  • +
  • TransactionNotFound: If no such transaction_id can be found in +self.ledger_file
  • +
+
@@ -168,7 +180,12 @@ without the prompt.

update_transaction(transaction)[source]
-
+

Update a transaction in the ledger file.

+

Takes a Transaction object and removes +the old transaction using transaction.id from the passed +Transaction instance and adds +transaction to the database.

+
@@ -190,6 +207,11 @@ without the prompt.

add_transaction(transaction)
+
+
+delete_transaction(transaction_id)
+
+
get_account(*args, **kw)[source]
diff --git a/doc/build/html/genindex.html b/doc/build/html/genindex.html index 91b8612..db08df0 100644 --- a/doc/build/html/genindex.html +++ b/doc/build/html/genindex.html @@ -243,6 +243,12 @@
delete_transaction() (accounting.storage.ledgercli.Ledger method)
+
+ +
(accounting.storage.Storage method) +
+ +
dict_to_object() (accounting.transport.AccountingDecoder method)
@@ -546,16 +552,20 @@
-
transaction_get() (in module accounting.web) +
transaction_delete() (in module accounting.web)
-
transaction_post() (in module accounting.web) +
transaction_get() (in module accounting.web)
+
transaction_post() (in module accounting.web) +
+ +
transaction_update() (in module accounting.web)
diff --git a/doc/build/html/objects.inv b/doc/build/html/objects.inv index f696297ecb241309c4f9ac33791e35582148b145..4afa71759f9f732076d301e9cd895fa5e170b326 100644 GIT binary patch delta 1000 zcmVbKu4GP0_5b>wckJnwozu9wlu#6hGXjPp!rk z?4S63Kf8Op?>1i*f8B;p6^Ikqo3A}^ho}K7vfSU;ee1V9cYhn694;nAw!$z`l@y3n z*hr?2ET+4bd%LUlrFr0O1UTi9Gxc^T?C-EAUvEjlx1VQpJi*q*$;j z^02nbcGe#_-ESE{NxQq$|M-;q?Y1ds9B2i~5+UtPC43NQ43i?(u9bOBqND%rae~Ea zseD-fE4d`EKz~uABmv#s1ZF8|aWkm9QJKp>j^3C!O_mK_!J2E>7{%tTf%EX&hn%_M zx&N)eAhWw8S@c8AI6T6!)kzk((jq}U=YX)P+S<}1Sz=GaKc)F@m#Kr?5;zQXOWV@M zbw;qUl3MIHW>yY$(F$@Jbc(UshB`pcUYB9o1fz}d%YOr&)ow8XbEnhU_T#K+xKpq5 z3n>tu%hn*uFqailU=xDSFr=Ia_p?wqllL9bI=34-0iJ(7qS=YFWROw1~^iWVSZMuM+sO%?ev%ap0 z^!Q%9Mt?D3&>V9th>N4^bsZi@Cm7844#XvQz0LJQ4nc&=m+!E^jfKEp6K}AU&Z@DT-`; zxUhZIGP;}%yzY&`J0h*pQX#U$q6)9awv$2c#(P1mVU(>)7P#dm{t;Osm!E86&FCCD zO@Ek!UNNGL3vLzjm4#=ou>DLd<|*RbK~Ef8ZXhF(oWB4G=FeVVbJ&oFS#LM&FJH72 zY_UT7GuJpW?DRNJsxeyn{@j-rVM1-<^3FqY)YD9`OBU31#`JfB)tDl;iMtonO;Xs{ zt3{hlP7O2Bj&pvL-Of&j2dVM3^QH8gZ(+hZkbM%W!d;wBn64b!zKh1;w(shH>r8w* z>d!luadQ~dvXnuLm)PQL8`1S%pet4I21;LQ^uG~2P>Kx-2-|?(l>F delta 1008 zcmVlWb*MCf8|q{1mA`X5iM1#ms&sBJyLDF>Q@vhO#==YN1d_#luQCWW5A%Xr(H zxhW#WmZf1E3WOmwud~SV4x7N06o>`Hf)X0Rac)is|GN6cl%hk{Vd7YI@Cs%p>Ed=U zkYhAY9uEH!CYV9Fk_5D#>%4yF6b!0vhq^q5KUp0wuJW8Sf*lNr6bNr*VGw1Q%M>ZF zvLrMNJ@3+Uj(>vIdd|HdHiF6$VT%iHd|j5|PoAW%=}$Pe;7(dxWAfaD{FgL8>}~2i z9n~dL^fd^Ih|Drv`kxd1MwaWnCoK|SV-(vI2Kt>JrEKckD@bEew#X?q5Hr(SaICT# zg&rnMMc@*|+NJ=RlxH5^Tl7mDFp(m-ZmQ7;5>s>aJAYKUYB4}YBDr}1;+?Vs_?|Fg z1_+I$F+rV<0djudnAtne+d`1bnH>-_yLlWbW<5{H@g5)=BE2H7ck;bb`U6-M7kz?T zJt9|37_`A03j%d{(*DQ~gb`6GAUu|5`o<-B1sbb5H|{gc#PDlQ6}a{9hj|JbNDa@b zI-neZ^?&&+R<7rXTs>Og26Taa$ad19y;jk;p@M9~ZPux@$e_yMEA^Ny ze9#+%cSH)Mr9xzfMG7>u&YW$^%2Mbqsx6G?^rgkktdqQGDcE+8x&y4AJ3Dl>3nt1f z8zSnVWf)woNqg>kjn}D_-Cv^j*+%l2Sy2W1+kbH~_K+@5x+~b)p=uXgcD-QjBaMEqQO&ZPPxkA3l#*Tdu*zk!HR5id%W@N6 zu4j|B3wGVT`M~SfWN75{En*+-5+TyK+#?MUOXBI<2Of)!G8#qZMb-R-1gxiXxyF|V z9sN^DA3TEl)L{e7V zi7#4c$2e{19mgBsi{4g;cICCwZm}*6o}96bW;v~O%=7)S ezEQ1tEWe5VbeC>y3wL2reYILqn*IknBk)