diff --git a/accounting/__init__.py b/accounting/__init__.py index 537ce83..17d7bb7 100644 --- a/accounting/__init__.py +++ b/accounting/__init__.py @@ -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()) diff --git a/accounting/transport.py b/accounting/transport.py new file mode 100644 index 0000000..091791b --- /dev/null +++ b/accounting/transport.py @@ -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) diff --git a/accounting/web.py b/accounting/web.py index c3e1d8f..4dd0478 100644 --- a/accounting/web.py +++ b/accounting/web.py @@ -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': + [, ] + [, ] + [] []>, + ] []>]>, + ] + [] + [] []>]>, + ] [ + ] []>]>]>, + , + ] + [] []>, + ] + [] []>]>, + ] []>]>]>]} + ''' + 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__':