diff --git a/conservancy_beancount/cliutil.py b/conservancy_beancount/cliutil.py index f8a289c31cd27e4f1de45bc67a52125221d8e52e..cc2f5deddd32f8827c7b9702f4324e5c9b62dca7 100644 --- a/conservancy_beancount/cliutil.py +++ b/conservancy_beancount/cliutil.py @@ -132,6 +132,26 @@ class LogLevel(enum.IntEnum): yield level.name.lower() +class ReturnFlag(enum.IntFlag): + """Common return codes for tools + + Tools should combine these flags to report different errors, and then use + ReturnFlag.returncode(flags) to report their final exit status code. + + Values 1, 2, 4, and 8 should be reserved for this class to be shared across + all tools. Flags 16, 32, and 64 are available for tools to report their own + specific errors. + """ + LOAD_ERRORS = 1 + NOTHING_TO_REPORT = 2 + _RESERVED4 = 4 + _RESERVED8 = 8 + + @classmethod + def returncode(cls, flags: int) -> int: + return 0 if flags == 0 else 16 + flags + + class SearchTerm(NamedTuple): """NamedTuple representing a user's metadata filter diff --git a/conservancy_beancount/reports/accrual.py b/conservancy_beancount/reports/accrual.py index 3b37372ad6756677b19fbca30171f5f15e7945fe..eb3b137f08a97e7af00ed18c069dc83ef5a80efe 100644 --- a/conservancy_beancount/reports/accrual.py +++ b/conservancy_beancount/reports/accrual.py @@ -635,13 +635,6 @@ class ReportType(enum.Enum): raise ValueError(f"unknown report type {name!r}") from None -class ReturnFlag(enum.IntFlag): - LOAD_ERRORS = 1 - # 2 was used in the past, it can probably be reclaimed. - REPORT_ERRORS = 4 - NOTHING_TO_REPORT = 8 - - def filter_search(postings: Iterable[data.Posting], search_terms: Iterable[cliutil.SearchTerm], ) -> Iterable[data.Posting]: @@ -722,14 +715,14 @@ def main(arglist: Optional[Sequence[str]]=None, filters.remove_opening_balance_txn(entries) for error in load_errors: bc_printer.print_error(error, file=stderr) - returncode |= ReturnFlag.LOAD_ERRORS + returncode |= cliutil.ReturnFlag.LOAD_ERRORS postings = list(filter_search( data.Posting.from_entries(entries), args.search_terms, )) if not postings: logger.warning("no matching entries found to report") - returncode |= ReturnFlag.NOTHING_TO_REPORT + returncode |= cliutil.ReturnFlag.NOTHING_TO_REPORT # groups is a mapping of metadata value strings to AccrualPostings. # The keys are basically arbitrary, the report classes don't rely on them, # but they do help symbolize what's being grouped. @@ -782,10 +775,10 @@ def main(arglist: Optional[Sequence[str]]=None, report = BalanceReport(out_file) if report is None: - returncode |= ReturnFlag.REPORT_ERRORS + returncode |= 16 else: report.run(groups) - return 0 if returncode == 0 else 16 + returncode + return cliutil.ReturnFlag.returncode(returncode) entry_point = cliutil.make_entry_point(__name__, PROGNAME) diff --git a/conservancy_beancount/reports/fund.py b/conservancy_beancount/reports/fund.py index ce2fb41f0c94445a2cf3f05244c8bec6dea83f77..c21e4358198e90db5bcdf9ed9c839d1d588e3b8a 100644 --- a/conservancy_beancount/reports/fund.py +++ b/conservancy_beancount/reports/fund.py @@ -283,11 +283,6 @@ class ReportType(enum.Enum): raise ValueError(f"no report type matches {s!r}") from None -class ReturnFlag(enum.IntFlag): - LOAD_ERRORS = 1 - NOTHING_TO_REPORT = 8 - - def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace: parser = argparse.ArgumentParser(prog=PROGNAME) cliutil.add_version_argument(parser) @@ -382,7 +377,7 @@ def main(arglist: Optional[Sequence[str]]=None, entries, load_errors, _ = books_loader.load_fy_range(args.start_date, args.stop_date) for error in load_errors: bc_printer.print_error(error, file=stderr) - returncode |= ReturnFlag.LOAD_ERRORS + returncode |= cliutil.ReturnFlag.LOAD_ERRORS postings = ( post @@ -403,7 +398,7 @@ def main(arglist: Optional[Sequence[str]]=None, ) if not fund_map: logger.warning("no matching postings found to report") - returncode |= ReturnFlag.NOTHING_TO_REPORT + returncode |= cliutil.ReturnFlag.NOTHING_TO_REPORT elif args.report_type is ReportType.TEXT: out_file = cliutil.text_output(args.output_file, stdout) report = TextReport(args.start_date, args.stop_date, out_file) @@ -419,7 +414,7 @@ def main(arglist: Optional[Sequence[str]]=None, logger.info("Writing report to %s", args.output_file) ods_file = cliutil.bytes_output(args.output_file, stdout) ods_report.save_file(ods_file) - return 0 if returncode == 0 else 16 + returncode + return cliutil.ReturnFlag.returncode(returncode) entry_point = cliutil.make_entry_point(__name__, PROGNAME) diff --git a/conservancy_beancount/reports/ledger.py b/conservancy_beancount/reports/ledger.py index d7636199bc3eb3ea52d1e5e2d2c638afa9ac44d7..6297e0efd0bc3ac3deaaf58f5db2c4ad4750b115 100644 --- a/conservancy_beancount/reports/ledger.py +++ b/conservancy_beancount/reports/ledger.py @@ -615,11 +615,6 @@ class TransactionODS(LedgerODS): ) -class ReturnFlag(enum.IntFlag): - LOAD_ERRORS = 1 - NOTHING_TO_REPORT = 8 - - class CashReportAction(argparse.Action): def __call__(self, parser: argparse.ArgumentParser, @@ -790,7 +785,7 @@ def main(arglist: Optional[Sequence[str]]=None, entries, load_errors, options = books_loader.load_fy_range(args.start_date, args.stop_date) for error in load_errors: bc_printer.print_error(error, file=stderr) - returncode |= ReturnFlag.LOAD_ERRORS + returncode |= cliutil.ReturnFlag.LOAD_ERRORS data.Account.load_from_books(entries, options) postings = data.Posting.from_entries(entries) @@ -828,7 +823,7 @@ def main(arglist: Optional[Sequence[str]]=None, report.write(postings) if not any(report.account_groups.values()): logger.warning("no matching postings found to report") - returncode |= ReturnFlag.NOTHING_TO_REPORT + returncode |= cliutil.ReturnFlag.NOTHING_TO_REPORT if args.output_file is None: out_dir_path = config.repository_path() or Path() @@ -840,7 +835,7 @@ def main(arglist: Optional[Sequence[str]]=None, logger.info("Writing report to %s", args.output_file) ods_file = cliutil.bytes_output(args.output_file, stdout) report.save_file(ods_file) - return 0 if returncode == 0 else 16 + returncode + return cliutil.ReturnFlag.returncode(returncode) entry_point = cliutil.make_entry_point(__name__, PROGNAME) diff --git a/conservancy_beancount/tools/opening_balances.py b/conservancy_beancount/tools/opening_balances.py index 43a339c88876acb352d772a0dbafb9f978ae8a31..22480839617da33f20ac49e67db32de80d73ab23 100644 --- a/conservancy_beancount/tools/opening_balances.py +++ b/conservancy_beancount/tools/opening_balances.py @@ -142,10 +142,6 @@ class Posting(data.Posting): ) -class ReturnFlag(enum.IntFlag): - LOAD_ERRORS = 1 - - def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace: parser = argparse.ArgumentParser(prog=PROGNAME) cliutil.add_version_argument(parser) @@ -199,7 +195,7 @@ def main(arglist: Optional[Sequence[str]]=None, entries, load_errors, _ = books_loader.load_fy_range(0, args.as_of_date) for error in load_errors: bc_printer.print_error(error, file=stderr) - returncode |= ReturnFlag.LOAD_ERRORS + returncode |= cliutil.ReturnFlag.LOAD_ERRORS inventories: Mapping[AccountWithFund, Inventory] = collections.defaultdict(Inventory) for post in Posting.from_entries(entries): @@ -251,7 +247,7 @@ def main(arglist: Optional[Sequence[str]]=None, dcontext = bc_dcontext.DisplayContext() dcontext.set_commas(True) bc_printer.print_entry(opening, dcontext, file=stdout) - return 0 if returncode == 0 else 16 + returncode + return cliutil.ReturnFlag.returncode(returncode) entry_point = cliutil.make_entry_point(__name__, PROGNAME) diff --git a/tests/test_reports_accrual.py b/tests/test_reports_accrual.py index c98295905cafd16d4e3cadb51bd05d10f8c82f38..c477e06a2c19e357c7e512dfe973fb1ffc9a2c56 100644 --- a/tests/test_reports_accrual.py +++ b/tests/test_reports_accrual.py @@ -775,7 +775,7 @@ def test_main_aging_report(arglist): check_aging_ods(output, datetime.date.today(), recv_rows, pay_rows) def test_main_no_books(): - errors = check_main_fails([], testutil.TestConfig(), 1 | 8) + errors = check_main_fails([], testutil.TestConfig(), 1 | 2) testutil.check_lines_match(iter(errors), [ r':[01]: +no books to load in configuration\b', ]) @@ -786,7 +786,7 @@ def test_main_no_books(): ['-t', 'balance', 'entity=NonExistent'], ]) def test_main_no_matches(arglist, caplog): - check_main_fails(arglist, None, 8) + check_main_fails(arglist, None, 2) testutil.check_logs_match(caplog, [ ('WARNING', 'no matching entries found to report'), ]) @@ -795,7 +795,7 @@ def test_main_no_rt(caplog): config = testutil.TestConfig( books_path=testutil.test_path('books/accruals.beancount'), ) - check_main_fails(['-t', 'out'], config, 4) + check_main_fails(['-t', 'out'], config, 16) testutil.check_logs_match(caplog, [ ('ERROR', 'unable to generate outgoing report: RT client is required'), ]) diff --git a/tests/test_reports_fund.py b/tests/test_reports_fund.py index a208f3099183b094bf040cdacd525b600bb14476..ff2beeb2a1c914b68054814942ef64dff935ae70 100644 --- a/tests/test_reports_fund.py +++ b/tests/test_reports_fund.py @@ -280,5 +280,5 @@ def test_ods_report(start_date, stop_date): def test_main_no_postings(caplog): retcode, output, errors = run_main(io.StringIO, ['NonexistentProject']) - assert retcode == 24 + assert retcode == 18 assert any(log.levelname == 'WARNING' for log in caplog.records) diff --git a/tests/test_reports_ledger.py b/tests/test_reports_ledger.py index 6d179917fe3c7b868cbf00af5d0dcff7c5cb4d0d..a3bf1e9d5b242781e93d2b25a84bba9529dfea4c 100644 --- a/tests/test_reports_ledger.py +++ b/tests/test_reports_ledger.py @@ -557,5 +557,5 @@ def test_main_invalid_account(caplog, arg): def test_main_no_postings(caplog): retcode, output, errors = run_main(['NonexistentProject']) - assert retcode == 24 + assert retcode == 18 assert any(log.levelname == 'WARNING' for log in caplog.records)