Two-way conversion from internal representation and JSON
This commit is contained in:
parent
4b5eca291b
commit
63c7b70000
3 changed files with 147 additions and 41 deletions
|
@ -43,12 +43,12 @@ class Ledger:
|
|||
process = self.get_process()
|
||||
|
||||
self.locked = True
|
||||
_log.debug('lock enabled')
|
||||
_log.debug('Lock enabled')
|
||||
|
||||
yield process
|
||||
|
||||
self.locked = False
|
||||
_log.debug('lock disabled')
|
||||
_log.debug('Lock disabled')
|
||||
|
||||
def assemble_arguments(self):
|
||||
return [
|
||||
|
@ -58,7 +58,7 @@ class Ledger:
|
|||
]
|
||||
|
||||
def init_process(self):
|
||||
_log.debug('starting ledger process')
|
||||
_log.debug('Starting ledger process...')
|
||||
self.ledger_process = subprocess.Popen(
|
||||
self.assemble_arguments(),
|
||||
stdout=subprocess.PIPE,
|
||||
|
@ -78,15 +78,12 @@ class Ledger:
|
|||
output = b''
|
||||
|
||||
while True:
|
||||
# _log.debug('reading data')
|
||||
|
||||
line = p.stdout.read(1) # XXX: This is a hack
|
||||
# _log.debug('line: %s', line)
|
||||
|
||||
output += line
|
||||
|
||||
if b'\n] ' in output:
|
||||
_log.debug('found prompt!')
|
||||
_log.debug('Found prompt!')
|
||||
break
|
||||
|
||||
output = output[:-3] # Cut away the prompt
|
||||
|
@ -107,6 +104,11 @@ class Ledger:
|
|||
|
||||
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
|
||||
|
||||
def bal(self):
|
||||
|
@ -131,8 +133,12 @@ class Ledger:
|
|||
|
||||
amounts = []
|
||||
|
||||
account_amounts = account.findall('./account-total/balance/amount') or \
|
||||
account.findall('./account-amount/amount')
|
||||
# Try to find an account total value, then try to find the account
|
||||
# balance
|
||||
account_amounts = account.findall(
|
||||
'./account-total/balance/amount') or \
|
||||
account.findall('./account-amount/amount') or \
|
||||
account.findall('./account-total/amount')
|
||||
|
||||
if account_amounts:
|
||||
for amount in account_amounts:
|
||||
|
@ -140,6 +146,8 @@ class Ledger:
|
|||
symbol = amount.find('./commodity/symbol').text
|
||||
|
||||
amounts.append(Amount(amount=quantity, symbol=symbol))
|
||||
else:
|
||||
_log.warning('Account %s does not have any amounts', name)
|
||||
|
||||
accounts.append(Account(name=name,
|
||||
amounts=amounts,
|
||||
|
@ -225,9 +233,18 @@ class Account:
|
|||
|
||||
|
||||
def main(argv=None):
|
||||
import argparse
|
||||
if argv is None:
|
||||
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')
|
||||
print(ledger.bal())
|
||||
print(ledger.reg())
|
||||
|
|
49
accounting/transport.py
Normal file
49
accounting/transport.py
Normal 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)
|
|
@ -1,46 +1,22 @@
|
|||
import sys
|
||||
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.transport import AccountingEncoder, AccountingDecoder
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
app = Flask('accounting')
|
||||
app.config.from_pyfile('config.py')
|
||||
|
||||
ledger = Ledger(ledger_file=app.config['LEDGER_FILE'])
|
||||
|
||||
|
||||
class AccountingEncoder(json.JSONEncoder):
|
||||
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)
|
||||
|
||||
|
||||
# These will convert output from our internal classes to JSON and back
|
||||
app.json_encoder = AccountingEncoder
|
||||
app.json_decoder = AccountingDecoder
|
||||
|
||||
|
||||
@app.route('/')
|
||||
|
@ -50,19 +26,83 @@ def index():
|
|||
|
||||
@app.route('/balance')
|
||||
def balance_report():
|
||||
''' Returns the balance report from ledger '''
|
||||
report_data = ledger.bal()
|
||||
|
||||
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')
|
||||
def register_report():
|
||||
''' Returns the register report from ledger '''
|
||||
report_data = ledger.reg()
|
||||
|
||||
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'])
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
Loading…
Reference in a new issue