cliutil: Add ReturnFlag.
Take this opportunity to re-standardize flag values now that it's clear what's most common.
This commit is contained in:
parent
46fe18809c
commit
4615364538
8 changed files with 37 additions and 38 deletions
|
@ -132,6 +132,26 @@ class LogLevel(enum.IntEnum):
|
||||||
yield level.name.lower()
|
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):
|
class SearchTerm(NamedTuple):
|
||||||
"""NamedTuple representing a user's metadata filter
|
"""NamedTuple representing a user's metadata filter
|
||||||
|
|
||||||
|
|
|
@ -635,13 +635,6 @@ class ReportType(enum.Enum):
|
||||||
raise ValueError(f"unknown report type {name!r}") from None
|
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],
|
def filter_search(postings: Iterable[data.Posting],
|
||||||
search_terms: Iterable[cliutil.SearchTerm],
|
search_terms: Iterable[cliutil.SearchTerm],
|
||||||
) -> Iterable[data.Posting]:
|
) -> Iterable[data.Posting]:
|
||||||
|
@ -722,14 +715,14 @@ def main(arglist: Optional[Sequence[str]]=None,
|
||||||
filters.remove_opening_balance_txn(entries)
|
filters.remove_opening_balance_txn(entries)
|
||||||
for error in load_errors:
|
for error in load_errors:
|
||||||
bc_printer.print_error(error, file=stderr)
|
bc_printer.print_error(error, file=stderr)
|
||||||
returncode |= ReturnFlag.LOAD_ERRORS
|
returncode |= cliutil.ReturnFlag.LOAD_ERRORS
|
||||||
|
|
||||||
postings = list(filter_search(
|
postings = list(filter_search(
|
||||||
data.Posting.from_entries(entries), args.search_terms,
|
data.Posting.from_entries(entries), args.search_terms,
|
||||||
))
|
))
|
||||||
if not postings:
|
if not postings:
|
||||||
logger.warning("no matching entries found to report")
|
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.
|
# groups is a mapping of metadata value strings to AccrualPostings.
|
||||||
# The keys are basically arbitrary, the report classes don't rely on them,
|
# The keys are basically arbitrary, the report classes don't rely on them,
|
||||||
# but they do help symbolize what's being grouped.
|
# but they do help symbolize what's being grouped.
|
||||||
|
@ -782,10 +775,10 @@ def main(arglist: Optional[Sequence[str]]=None,
|
||||||
report = BalanceReport(out_file)
|
report = BalanceReport(out_file)
|
||||||
|
|
||||||
if report is None:
|
if report is None:
|
||||||
returncode |= ReturnFlag.REPORT_ERRORS
|
returncode |= 16
|
||||||
else:
|
else:
|
||||||
report.run(groups)
|
report.run(groups)
|
||||||
return 0 if returncode == 0 else 16 + returncode
|
return cliutil.ReturnFlag.returncode(returncode)
|
||||||
|
|
||||||
entry_point = cliutil.make_entry_point(__name__, PROGNAME)
|
entry_point = cliutil.make_entry_point(__name__, PROGNAME)
|
||||||
|
|
||||||
|
|
|
@ -283,11 +283,6 @@ class ReportType(enum.Enum):
|
||||||
raise ValueError(f"no report type matches {s!r}") from None
|
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:
|
def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(prog=PROGNAME)
|
parser = argparse.ArgumentParser(prog=PROGNAME)
|
||||||
cliutil.add_version_argument(parser)
|
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)
|
entries, load_errors, _ = books_loader.load_fy_range(args.start_date, args.stop_date)
|
||||||
for error in load_errors:
|
for error in load_errors:
|
||||||
bc_printer.print_error(error, file=stderr)
|
bc_printer.print_error(error, file=stderr)
|
||||||
returncode |= ReturnFlag.LOAD_ERRORS
|
returncode |= cliutil.ReturnFlag.LOAD_ERRORS
|
||||||
|
|
||||||
postings = (
|
postings = (
|
||||||
post
|
post
|
||||||
|
@ -403,7 +398,7 @@ def main(arglist: Optional[Sequence[str]]=None,
|
||||||
)
|
)
|
||||||
if not fund_map:
|
if not fund_map:
|
||||||
logger.warning("no matching postings found to report")
|
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:
|
elif args.report_type is ReportType.TEXT:
|
||||||
out_file = cliutil.text_output(args.output_file, stdout)
|
out_file = cliutil.text_output(args.output_file, stdout)
|
||||||
report = TextReport(args.start_date, args.stop_date, out_file)
|
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)
|
logger.info("Writing report to %s", args.output_file)
|
||||||
ods_file = cliutil.bytes_output(args.output_file, stdout)
|
ods_file = cliutil.bytes_output(args.output_file, stdout)
|
||||||
ods_report.save_file(ods_file)
|
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)
|
entry_point = cliutil.make_entry_point(__name__, PROGNAME)
|
||||||
|
|
||||||
|
|
|
@ -615,11 +615,6 @@ class TransactionODS(LedgerODS):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ReturnFlag(enum.IntFlag):
|
|
||||||
LOAD_ERRORS = 1
|
|
||||||
NOTHING_TO_REPORT = 8
|
|
||||||
|
|
||||||
|
|
||||||
class CashReportAction(argparse.Action):
|
class CashReportAction(argparse.Action):
|
||||||
def __call__(self,
|
def __call__(self,
|
||||||
parser: argparse.ArgumentParser,
|
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)
|
entries, load_errors, options = books_loader.load_fy_range(args.start_date, args.stop_date)
|
||||||
for error in load_errors:
|
for error in load_errors:
|
||||||
bc_printer.print_error(error, file=stderr)
|
bc_printer.print_error(error, file=stderr)
|
||||||
returncode |= ReturnFlag.LOAD_ERRORS
|
returncode |= cliutil.ReturnFlag.LOAD_ERRORS
|
||||||
|
|
||||||
data.Account.load_from_books(entries, options)
|
data.Account.load_from_books(entries, options)
|
||||||
postings = data.Posting.from_entries(entries)
|
postings = data.Posting.from_entries(entries)
|
||||||
|
@ -828,7 +823,7 @@ def main(arglist: Optional[Sequence[str]]=None,
|
||||||
report.write(postings)
|
report.write(postings)
|
||||||
if not any(report.account_groups.values()):
|
if not any(report.account_groups.values()):
|
||||||
logger.warning("no matching postings found to report")
|
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:
|
if args.output_file is None:
|
||||||
out_dir_path = config.repository_path() or Path()
|
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)
|
logger.info("Writing report to %s", args.output_file)
|
||||||
ods_file = cliutil.bytes_output(args.output_file, stdout)
|
ods_file = cliutil.bytes_output(args.output_file, stdout)
|
||||||
report.save_file(ods_file)
|
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)
|
entry_point = cliutil.make_entry_point(__name__, PROGNAME)
|
||||||
|
|
||||||
|
|
|
@ -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:
|
def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(prog=PROGNAME)
|
parser = argparse.ArgumentParser(prog=PROGNAME)
|
||||||
cliutil.add_version_argument(parser)
|
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)
|
entries, load_errors, _ = books_loader.load_fy_range(0, args.as_of_date)
|
||||||
for error in load_errors:
|
for error in load_errors:
|
||||||
bc_printer.print_error(error, file=stderr)
|
bc_printer.print_error(error, file=stderr)
|
||||||
returncode |= ReturnFlag.LOAD_ERRORS
|
returncode |= cliutil.ReturnFlag.LOAD_ERRORS
|
||||||
|
|
||||||
inventories: Mapping[AccountWithFund, Inventory] = collections.defaultdict(Inventory)
|
inventories: Mapping[AccountWithFund, Inventory] = collections.defaultdict(Inventory)
|
||||||
for post in Posting.from_entries(entries):
|
for post in Posting.from_entries(entries):
|
||||||
|
@ -251,7 +247,7 @@ def main(arglist: Optional[Sequence[str]]=None,
|
||||||
dcontext = bc_dcontext.DisplayContext()
|
dcontext = bc_dcontext.DisplayContext()
|
||||||
dcontext.set_commas(True)
|
dcontext.set_commas(True)
|
||||||
bc_printer.print_entry(opening, dcontext, file=stdout)
|
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)
|
entry_point = cliutil.make_entry_point(__name__, PROGNAME)
|
||||||
|
|
||||||
|
|
|
@ -775,7 +775,7 @@ def test_main_aging_report(arglist):
|
||||||
check_aging_ods(output, datetime.date.today(), recv_rows, pay_rows)
|
check_aging_ods(output, datetime.date.today(), recv_rows, pay_rows)
|
||||||
|
|
||||||
def test_main_no_books():
|
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), [
|
testutil.check_lines_match(iter(errors), [
|
||||||
r':[01]: +no books to load in configuration\b',
|
r':[01]: +no books to load in configuration\b',
|
||||||
])
|
])
|
||||||
|
@ -786,7 +786,7 @@ def test_main_no_books():
|
||||||
['-t', 'balance', 'entity=NonExistent'],
|
['-t', 'balance', 'entity=NonExistent'],
|
||||||
])
|
])
|
||||||
def test_main_no_matches(arglist, caplog):
|
def test_main_no_matches(arglist, caplog):
|
||||||
check_main_fails(arglist, None, 8)
|
check_main_fails(arglist, None, 2)
|
||||||
testutil.check_logs_match(caplog, [
|
testutil.check_logs_match(caplog, [
|
||||||
('WARNING', 'no matching entries found to report'),
|
('WARNING', 'no matching entries found to report'),
|
||||||
])
|
])
|
||||||
|
@ -795,7 +795,7 @@ def test_main_no_rt(caplog):
|
||||||
config = testutil.TestConfig(
|
config = testutil.TestConfig(
|
||||||
books_path=testutil.test_path('books/accruals.beancount'),
|
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, [
|
testutil.check_logs_match(caplog, [
|
||||||
('ERROR', 'unable to generate outgoing report: RT client is required'),
|
('ERROR', 'unable to generate outgoing report: RT client is required'),
|
||||||
])
|
])
|
||||||
|
|
|
@ -280,5 +280,5 @@ def test_ods_report(start_date, stop_date):
|
||||||
|
|
||||||
def test_main_no_postings(caplog):
|
def test_main_no_postings(caplog):
|
||||||
retcode, output, errors = run_main(io.StringIO, ['NonexistentProject'])
|
retcode, output, errors = run_main(io.StringIO, ['NonexistentProject'])
|
||||||
assert retcode == 24
|
assert retcode == 18
|
||||||
assert any(log.levelname == 'WARNING' for log in caplog.records)
|
assert any(log.levelname == 'WARNING' for log in caplog.records)
|
||||||
|
|
|
@ -557,5 +557,5 @@ def test_main_invalid_account(caplog, arg):
|
||||||
|
|
||||||
def test_main_no_postings(caplog):
|
def test_main_no_postings(caplog):
|
||||||
retcode, output, errors = run_main(['NonexistentProject'])
|
retcode, output, errors = run_main(['NonexistentProject'])
|
||||||
assert retcode == 24
|
assert retcode == 18
|
||||||
assert any(log.levelname == 'WARNING' for log in caplog.records)
|
assert any(log.levelname == 'WARNING' for log in caplog.records)
|
||||||
|
|
Loading…
Reference in a new issue