diff --git a/bookeeper/__main__.py b/bookeeper/__main__.py index 9ef4a81..144a1a8 100644 --- a/bookeeper/__main__.py +++ b/bookeeper/__main__.py @@ -3,73 +3,51 @@ import sqlite3 import sys from . import args -from . import calc from . import db from . import parser from . import util +from .ctx import Context + + +dbconn = None +dbcur = None +ctx = None try: + parg = args.create() try: # parsing args - params = args.parse(sys.argv[1:]) + params = parg.parse_args(sys.argv[1:]) + if params.actions is None: + params.actions = [] except Exception as e: - args.print_help() + parg.print_help() raise Exception("failure while parsing args", e) try: # initializing DB dbconn = sqlite3.connect(params.dbpath) dbcur = dbconn.cursor() db.try_init(dbcur) - dbcur.execute("BEGIN;") except Exception as e: raise Exception("failure while initializing DB", e) - if not sys.stdin.isatty(): - try: # insert new records - for tx in parser.parse(sys.stdin): - db.apply(dbcur, tx) + ctx = Context(dbcur) + for action in enumerate(params.actions): + try: + action[1](ctx) except Exception as e: - raise Exception("failure while inserting new records", e) - - try: # writing outputs - calc.call( - calc.balancesheet, - params.balancesheet, - dbcur, - ) - calc.call( - calc.plstatement, - util.get_or(params.plstatement, 0), - dbcur, - begin=util.get_or(params.plstatement, 1), - end =util.get_or(params.plstatement, 2), - ) - calc.call( - calc.balancetransition, - params.balancetransition, - dbcur, - ) - calc.call( - calc.pltransition, - util.get_or(params.pltransition, 0), - dbcur, - datefmt=util.get_or(params.pltransition, 1), - ) - except Exception as e: - raise Exception("failure while calculating output", e) + raise Exception(f"failure while executing {action[0]}-th action", e) except Exception as e: print(f"bookeeper: error: {e}", file=sys.stderr) - dbcur and dbcur.execute("ROLLBACK;") -else: - if params.permanentize: - dbcur.execute("COMMIT;") - else: - dbcur.execute("ROLLBACK;") finally: + if ctx: + ctx.finalize() + if dbcur: dbcur.close() + if dbconn: dbconn.commit(); dbconn.close() diff --git a/bookeeper/args.py b/bookeeper/args.py index 10571c0..94089d6 100644 --- a/bookeeper/args.py +++ b/bookeeper/args.py @@ -1,55 +1,68 @@ import argparse -_parser = argparse.ArgumentParser( - prog="bookeeper", - description="CLI app for bookkeeping", - epilog="developed by falsycat ", -) +def create(): + p = argparse.ArgumentParser( + prog="bookeeper", + description="CLI app for bookkeeping", + epilog="developed by falsycat ", + ) -_parser.add_argument( - "dbpath", - help="path to sqlite database file", - nargs="?", - default=":memory:", -) + p.add_argument( + "dbpath", + help="path to sqlite database file", + nargs="?", + default=":memory:", + ) -_parser.add_argument( - "-p", "--permanentize", - help="apply changes", - action="store_true", - dest="permanentize", -) + p.add_argument( + "-p", "--permanentize", + help="apply changes", + nargs=1, + action=_PermanentizeAction, + dest="actions", + ) + p.add_argument( + "-r", "--read", + help="reads all records from the file", + nargs=1, + action=_ReadRecordsAction, + dest="actions", + ) + p.add_argument( + "-o", "--output", + help="changes output file for latter actions", + nargs=1, + action=_ChangeOutputAction, + dest="actions", + ) + p.add_argument( + "--sql", + help="executes SQL command", + nargs=1, + action=_SqlAction, + dest="actions", + ) + return p -_parser.add_argument( - "-bs", "--balancesheet", - help="emit balancesheet", - action="store", - dest="balancesheet", -) -_parser.add_argument( - "-bt", "--balance-transition", - help="emit daily transition of balance", - action="store", - dest="balancetransition", -) -_parser.add_argument( - "-pl", "--pl-statement", - help="emit P/L statement (filepath, begin, end)", - action="store", - nargs="+", - dest="plstatement", -) -_parser.add_argument( - "-plt", "--pl-transition", - help="emit transition of P/L", - action="store", - nargs="+", - dest="pltransition", -) +class _PermanentizeAction(argparse.Action): + def __call__(self, parser, ns, values, option_string=None): + _push_action(ns, lambda x: x.permanentize()) -def print_help(): - _parser.print_help() +class _ReadRecordsAction(argparse.Action): + def __call__(self, parser, ns, values, option_string=None): + _push_action(ns, lambda x: x.read_records(values[0])) -def parse(args): - ret = _parser.parse_args(args) - return ret +class _ChangeOutputAction(argparse.Action): + def __call__(self, parser, ns, values, option_string=None): + _push_action(ns, lambda x: x.change_output(values[0])) + +class _SqlAction(argparse.Action): + def __call__(self, parser, ns, values, option_string=None): + _push_action(ns, lambda x: x.sql(values[0])) + +def _push_action(ns, act): + actions = getattr(ns, "actions", None) + if actions is None: + actions = [] + setattr(ns, "actions", actions) + actions.append(act) diff --git a/bookeeper/calc.py b/bookeeper/calc.py deleted file mode 100644 index 30ea070..0000000 --- a/bookeeper/calc.py +++ /dev/null @@ -1,101 +0,0 @@ -import sys - -def call(func, path, *args, **kwargs): - if path == "-": - func(sys.stdout, *args, **kwargs) - elif path is None: - pass - else: - with open(path, "w") as f: - func(f, *args, **kwargs) - -def balancesheet(stream, cur): - cur.execute(""" - SELECT - entry.type, - entry.name, - SUM(journal.amount) - FROM journal RIGHT JOIN entry ON journal.entry=entry.id - WHERE entry.type in ("A","D","N") - GROUP BY entry.name; - """) - for row in cur: - print(_recover_entry_name(row[0], row[1]), row[2], file=stream, sep=",") - -def plstatement(stream, cur, begin=None, end=None): - if begin is None: - begin = "0000-00-00"; - if end is None: - end = "9999-12-31"; - - cur.execute(""" - SELECT - entry.type, - entry.name, - SUM(journal.amount) - FROM journal - RIGHT JOIN entry ON journal.entry=entry.id - RIGHT JOIN tx ON journal.tx =tx.id - WHERE - entry.type in ("E","R") AND tx.date BETWEEN ? AND ? - GROUP BY entry.name; - """, (begin, end)) - for row in cur: - print(_recover_entry_name(row[0], row[1]), row[2], file=stream, sep=",") - -def balancetransition(stream, cur): - cur.execute(""" - SELECT - date, - SUM(sum) - OVER(ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) - FROM ( - SELECT - tx.date AS date, - SUM( - CASE WHEN entry.type="A" THEN journal.amount ELSE -journal.amount END - ) AS sum - FROM journal - RIGHT JOIN entry ON journal.entry=entry.id - RIGHT JOIN tx ON journal.tx =tx.id - WHERE entry.type in ("A","D","N") - GROUP BY tx.date - ); - """) - for row in cur: - print(*row, file=stream, sep=",") - -def pltransition(stream, cur, datefmt=None): - if datefmt is None: - datefmt = "%Y-%m-%d" - cur.execute(""" - WITH temp AS ( - SELECT - STRFTIME(?, tx.date) AS period, - entry.type AS type, - SUM(amount) AS sum - FROM journal - RIGHT JOIN entry ON journal.entry=entry.id - RIGHT JOIN tx ON journal.tx =tx.id - WHERE entry.type in ("E","R") - GROUP BY period, entry.type - ) - SELECT - a.period AS period, - a.sum AS expense, - IFNULL(b.sum, 0) AS revenue - FROM temp AS a LEFT JOIN temp AS b ON a.period=b.period AND b.type="R" - WHERE a.type="E"; - """, [datefmt]) - for row in cur: - print(*row, file=stream, sep=",") - -def _recover_entry_name(t: str, name: str): - m = { - "A": "asset", - "D": "debt", - "N": "net", - "E": "expense", - "R": "revenue", - } - return f"{m[t]}/{name}" diff --git a/bookeeper/ctx.py b/bookeeper/ctx.py new file mode 100644 index 0000000..8f5675f --- /dev/null +++ b/bookeeper/ctx.py @@ -0,0 +1,46 @@ +import io +import sqlite3 +import sys + +from . import db +from . import parser + +class Context: + def __init__(self, dbcur: sqlite3.Cursor): + self.ostream = sys.stdout + self.dbcur = dbcur + self.dbcur.execute("BEGIN;") + + def finalize(self): + self.dbcur.execute("ROLLBACK;") + self.close_output() + + def permanentize(self): + self.dbcur.execute("COMMIT;") + self.dbcur.execute("BEGIN;") + + def read_records(self, path): + def parse(st): + for tx in parser.parse(st): + db.apply(self.dbcur, tx) + + if path == "-": + parse(sys.stdin) + else: + with open(path) as f: + parse(f) + + def change_output(self, path): + self.close_output() + if path != "-": + self.ostream = open(path, "w") + + def close_output(self): + if self.ostream is not sys.stdout: + self.ostream.close() + self.ostream = sys.stdout + + def sql(self, cmd: str): + self.dbcur.execute(cmd) + for row in self.dbcur: + print(*row, file=self.ostream) diff --git a/bookeeper/util.py b/bookeeper/util.py deleted file mode 100644 index 2037e4d..0000000 --- a/bookeeper/util.py +++ /dev/null @@ -1,5 +0,0 @@ -def get_or(a: list, idx: int, default = None): - try: - return a[idx] - except: - return default