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()
|
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
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 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__':
|
||||||
|
|
Loading…
Reference in a new issue