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) |