Two-way conversion from internal representation and JSON

This commit is contained in:
Joar Wandborg 2013-12-10 23:22:57 +01:00
parent 4b5eca291b
commit 63c7b70000
3 changed files with 147 additions and 41 deletions

View file

@ -43,12 +43,12 @@ class Ledger:
process = self.get_process() process = self.get_process()
self.locked = True self.locked = True
_log.debug('lock enabled') _log.debug('Lock enabled')
yield process yield process
self.locked = False self.locked = False
_log.debug('lock disabled') _log.debug('Lock disabled')
def assemble_arguments(self): def assemble_arguments(self):
return [ return [
@ -58,7 +58,7 @@ class Ledger:
] ]
def init_process(self): def init_process(self):
_log.debug('starting ledger process') _log.debug('Starting ledger process...')
self.ledger_process = subprocess.Popen( self.ledger_process = subprocess.Popen(
self.assemble_arguments(), self.assemble_arguments(),
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
@ -78,15 +78,12 @@ class Ledger:
output = b'' output = b''
while True: while True:
# _log.debug('reading data')
line = p.stdout.read(1) # XXX: This is a hack line = p.stdout.read(1) # XXX: This is a hack
# _log.debug('line: %s', line)
output += line output += line
if b'\n] ' in output: if b'\n] ' in output:
_log.debug('found prompt!') _log.debug('Found prompt!')
break break
output = output[:-3] # Cut away the prompt output = output[:-3] # Cut away the prompt
@ -107,6 +104,11 @@ class Ledger:
output = self.read_until_prompt(p) output = self.read_until_prompt(p)
self.ledger_process.send_signal(subprocess.signal.SIGTERM)
_log.debug('Waiting for ledger to shut down')
self.ledger_process.wait()
self.ledger_process = None
return output return output
def bal(self): def bal(self):
@ -131,8 +133,12 @@ class Ledger:
amounts = [] amounts = []
account_amounts = account.findall('./account-total/balance/amount') or \ # Try to find an account total value, then try to find the account
account.findall('./account-amount/amount') # balance
account_amounts = account.findall(
'./account-total/balance/amount') or \
account.findall('./account-amount/amount') or \
account.findall('./account-total/amount')
if account_amounts: if account_amounts:
for amount in account_amounts: for amount in account_amounts:
@ -140,6 +146,8 @@ class Ledger:
symbol = amount.find('./commodity/symbol').text symbol = amount.find('./commodity/symbol').text
amounts.append(Amount(amount=quantity, symbol=symbol)) amounts.append(Amount(amount=quantity, symbol=symbol))
else:
_log.warning('Account %s does not have any amounts', name)
accounts.append(Account(name=name, accounts.append(Account(name=name,
amounts=amounts, amounts=amounts,
@ -225,9 +233,18 @@ class Account:
def main(argv=None): def main(argv=None):
import argparse
if argv is None: if argv is None:
argv = sys.argv argv = sys.argv
logging.basicConfig(level=logging.INFO)
parser = argparse.ArgumentParser()
parser.add_argument('-v', '--verbosity',
default='INFO',
help=('Filter logging output. Possible values:' +
' CRITICAL, ERROR, WARNING, INFO, DEBUG'))
args = parser.parse_args(argv[1:])
logging.basicConfig(level=getattr(logging, args.verbosity, 'INFO'))
ledger = Ledger(ledger_file='non-profit-test-data.ledger') ledger = Ledger(ledger_file='non-profit-test-data.ledger')
print(ledger.bal()) print(ledger.bal())
print(ledger.reg()) print(ledger.reg())

49
accounting/transport.py Normal file
View file

@ -0,0 +1,49 @@
from flask import json
from accounting import Amount, Transaction, Posting, Account
class AccountingEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Account):
return dict(
__type__=o.__class__.__name__,
name=o.name,
amounts=o.amounts,
accounts=o.accounts
)
elif isinstance(o, Transaction):
return dict(
__type__=o.__class__.__name__,
date=o.date.strftime('%Y-%m-%d'),
payee=o.payee,
postings=o.postings
)
elif isinstance(o, Posting):
return dict(
__type__=o.__class__.__name__,
account=o.account,
amount=o.amount,
)
elif isinstance(o, Amount):
return dict(
__type__=o.__class__.__name__,
amount=o.amount,
symbol=o.symbol
)
return json.JSONEncoder.default(self, o)
class AccountingDecoder(json.JSONDecoder):
def __init__(self):
json.JSONDecoder.__init__(self, object_hook=self.dict_to_object)
def dict_to_object(self, d):
if '__type__' not in d:
return d
types = {c.__name__ : c for c in [Amount, Transaction, Posting,
Account]}
_type = d.pop('__type__')
return types[_type](**d)

View file

@ -1,46 +1,22 @@
import sys
import logging import logging
import argparse
from flask import Flask, g, jsonify, json from flask import Flask, g, jsonify, json, request
from accounting import Ledger, Account, Posting, Transaction, Amount from accounting import Ledger, Account, Posting, Transaction, Amount
from accounting.transport import AccountingEncoder, AccountingDecoder
logging.basicConfig(level=logging.DEBUG)
app = Flask('accounting') app = Flask('accounting')
app.config.from_pyfile('config.py') app.config.from_pyfile('config.py')
ledger = Ledger(ledger_file=app.config['LEDGER_FILE']) ledger = Ledger(ledger_file=app.config['LEDGER_FILE'])
class AccountingEncoder(json.JSONEncoder): # These will convert output from our internal classes to JSON and back
def default(self, o):
if isinstance(o, Account):
return dict(
name=o.name,
amounts=o.amounts,
accounts=o.accounts
)
elif isinstance(o, Transaction):
return dict(
date=o.date.strftime('%Y-%m-%d'),
payee=o.payee,
postings=o.postings
)
elif isinstance(o, Posting):
return dict(
account=o.account,
amount=o.amount,
)
elif isinstance(o, Amount):
return dict(
amount=o.amount,
symbol=o.symbol
)
return json.JSONEncoder.default(self, o)
app.json_encoder = AccountingEncoder app.json_encoder = AccountingEncoder
app.json_decoder = AccountingDecoder
@app.route('/') @app.route('/')
@ -50,19 +26,83 @@ def index():
@app.route('/balance') @app.route('/balance')
def balance_report(): def balance_report():
''' Returns the balance report from ledger '''
report_data = ledger.bal() report_data = ledger.bal()
return jsonify(balance_report=report_data) return jsonify(balance_report=report_data)
@app.route('/parse-json', methods=['POST'])
def parse_json():
r'''
Parses a __type__-annotated JSON payload and debug-logs the decoded version
of it.
Example:
wget http://127.0.0.1:5000/balance -O balance.json
curl -X POST -H 'Content-Type: application/json' -d @balance.json \
http://127.0.0.1/parse-json
# Logging output (linebreaks added for clarity)
DEBUG:accounting:json data: {'balance_report':
[<Account "None" [
<Amount $ 0>, <Amount KARMA 0>]
[<Account "Assets" [
<Amount $ 50>, <Amount KARMA 10>]
[<Account "Assets:Checking" [
<Amount $ 50>] []>,
<Account "Assets:Karma Account" [
<Amount KARMA 10>] []>]>,
<Account "Expenses" [
<Amount $ 500>]
[<Account "Expenses:Blah" [
<Amount $ 250>]
[<Account "Expenses:Blah:Hosting" [
<Amount $ 250>] []>]>,
<Account "Expenses:Foo" [
<Amount $ 250>] [
<Account "Expenses:Foo:Hosting" [
<Amount $ 250>] []>]>]>,
<Account "Income" [
<Amount $ -550>,
<Amount KARMA -10>]
[<Account "Income:Donation" [
<Amount $ -50>] []>,
<Account "Income:Foo" [
<Amount $ -500>]
[<Account "Income:Foo:Donation" [
<Amount $ -500>] []>]>,
<Account "Income:Karma" [
<Amount KARMA -10>] []>]>]>]}
'''
app.logger.debug('json data: %s', request.json)
return jsonify(foo='bar')
@app.route('/register') @app.route('/register')
def register_report(): def register_report():
''' Returns the register report from ledger '''
report_data = ledger.reg() report_data = ledger.reg()
return jsonify(register_report=report_data) return jsonify(register_report=report_data)
def main(): def main(argv=None):
prog = __name__
if argv is None:
prog = sys.argv[0]
argv = sys.argv[1:]
parser = argparse.ArgumentParser(prog=prog)
parser.add_argument('-v', '--verbosity',
default='INFO',
help=('Filter logging output. Possible values:' +
' CRITICAL, ERROR, WARNING, INFO, DEBUG'))
args = parser.parse_args(argv)
logging.basicConfig(level=getattr(logging, args.verbosity, 'INFO'))
app.run(host=app.config['HOST'], port=app.config['PORT']) app.run(host=app.config['HOST'], port=app.config['PORT'])
if __name__ == '__main__': if __name__ == '__main__':