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()) diff --git a/setup.py b/setup.py index e3c7fec902bb5cb8ed58547b53947d92c34e338f..d5cafbef38d15a842ab3311f3fe3d518b8b536cd 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup setup( name='conservancy_beancount', description="Plugin, library, and reports for reading Conservancy's books", - version='1.11.1', + version='1.12.0', author='Software Freedom Conservancy', author_email='info@sfconservancy.org', license='GNU AGPLv3+', @@ -40,6 +40,7 @@ setup( 'accrual-report = conservancy_beancount.reports.accrual:entry_point', 'assemble-audit-reports = conservancy_beancount.tools.audit_report:entry_point', 'balance-sheet-report = conservancy_beancount.reports.balance_sheet:entry_point', + 'bean-sort = conservancy_beancount.tools.sort_entries:entry_point', 'extract-odf-links = conservancy_beancount.tools.extract_odf_links:entry_point', 'fund-report = conservancy_beancount.reports.fund:entry_point', 'ledger-report = conservancy_beancount.reports.ledger:entry_point',