diff --git a/conservancy_beancount/tools/sort_entries.py b/conservancy_beancount/tools/sort_entries.py new file mode 100644 index 0000000000000000000000000000000000000000..a3bf26048c3fc7f1e9281282619ba9243cb34d03 --- /dev/null +++ b/conservancy_beancount/tools/sort_entries.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""sort_entries.py - Consistently sort and format Beancount entries + +This is useful to use as a preprocessing step before comparing entries with +tools like ``diff``. +""" +# Copyright © 2020 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import argparse +import logging +import os +import sys + +from pathlib import Path + +from beancount.core import display_context as bc_dcontext +from beancount import loader as bc_loader +from beancount.parser import printer as bc_printer + +from typing import ( + Hashable, + Optional, + Sequence, + TextIO, +) +from ..beancount_types import ( + Directive, + Entries, + Errors, +) + +from .. import cliutil + +PROGNAME = 'bean-sort' +logger = logging.getLogger('conservancy_beancount.tools.sort_entries') + +def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace: + parser = argparse.ArgumentParser(prog=PROGNAME) + cliutil.add_version_argument(parser) + cliutil.add_loglevel_argument(parser) + parser.add_argument( + '--quiet', '-q', + action='store_true', + help="""Suppress Beancount errors +""") + parser.add_argument( + 'paths', + metavar='PATH', + type=Path, + nargs=argparse.ONE_OR_MORE, + help="""Beancount path(s) to read entries from +""") + return parser.parse_args(arglist) + +def entry_sorter(entry: Directive) -> Hashable: + type_name = type(entry).__name__ + if type_name == 'Transaction': + return (entry.date, type_name, entry.narration, entry.payee or '') # type:ignore[attr-defined] + else: + return (entry.date, type_name) + +def main(arglist: Optional[Sequence[str]]=None, + stdout: TextIO=sys.stdout, + stderr: TextIO=sys.stderr, +) -> int: + args = parse_arguments(arglist) + cliutil.set_loglevel(logger, args.loglevel) + + entries: Entries = [] + errors: Errors = [] + for path in args.paths: + new_entries, new_errors, _ = bc_loader.load_file(path) + entries.extend(new_entries) + errors.extend(new_errors) + entries.sort(key=entry_sorter) + if not args.quiet: + for error in errors: + bc_printer.print_error(error, file=stderr) + dcontext = bc_dcontext.DisplayContext() + dcontext.set_commas(True) + bc_printer.print_entries(entries, dcontext, file=stdout) + return os.EX_DATAERR if errors and not args.quiet else os.EX_OK + +entry_point = cliutil.make_entry_point(__name__, PROGNAME) + +if __name__ == '__main__': + exit(entry_point())