accrual: Introduce logging infrastructure.
This commit is contained in:
parent
8b2683d962
commit
d3e0a38073
2 changed files with 43 additions and 34 deletions
|
@ -62,6 +62,7 @@ import argparse
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
import enum
|
import enum
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -98,6 +99,8 @@ from .. import rtutil
|
||||||
PostGroups = Mapping[Optional[MetaValue], core.RelatedPostings]
|
PostGroups = Mapping[Optional[MetaValue], core.RelatedPostings]
|
||||||
RTObject = Mapping[str, str]
|
RTObject = Mapping[str, str]
|
||||||
|
|
||||||
|
_logger = logging.getLogger('conservancy_beancount.reports.accrual')
|
||||||
|
|
||||||
class Account(NamedTuple):
|
class Account(NamedTuple):
|
||||||
name: str
|
name: str
|
||||||
balance_paid: Callable[[core.Balance], bool]
|
balance_paid: Callable[[core.Balance], bool]
|
||||||
|
@ -129,9 +132,9 @@ class AccrualAccount(enum.Enum):
|
||||||
|
|
||||||
|
|
||||||
class BaseReport:
|
class BaseReport:
|
||||||
def __init__(self, out_file: TextIO, err_file: TextIO) -> None:
|
def __init__(self, out_file: TextIO) -> None:
|
||||||
self.out_file = out_file
|
self.out_file = out_file
|
||||||
self.err_file = err_file
|
self.logger = _logger.getChild(type(self).__name__)
|
||||||
|
|
||||||
def _since_last_nonzero(self, posts: core.RelatedPostings) -> core.RelatedPostings:
|
def _since_last_nonzero(self, posts: core.RelatedPostings) -> core.RelatedPostings:
|
||||||
retval = core.RelatedPostings()
|
retval = core.RelatedPostings()
|
||||||
|
@ -170,11 +173,10 @@ class BalanceReport(BaseReport):
|
||||||
|
|
||||||
|
|
||||||
class OutgoingReport(BaseReport):
|
class OutgoingReport(BaseReport):
|
||||||
def __init__(self, rt_client: rt.Rt, out_file: TextIO, err_file: TextIO) -> None:
|
def __init__(self, rt_client: rt.Rt, out_file: TextIO) -> None:
|
||||||
|
super().__init__(out_file)
|
||||||
self.rt_client = rt_client
|
self.rt_client = rt_client
|
||||||
self.rt_wrapper = rtutil.RT(rt_client)
|
self.rt_wrapper = rtutil.RT(rt_client)
|
||||||
self.out_file = out_file
|
|
||||||
self.err_file = err_file
|
|
||||||
|
|
||||||
def _primary_rt_id(self, posts: core.RelatedPostings) -> rtutil.TicketAttachmentIds:
|
def _primary_rt_id(self, posts: core.RelatedPostings) -> rtutil.TicketAttachmentIds:
|
||||||
rt_ids = posts.all_meta_links('rt-id')
|
rt_ids = posts.all_meta_links('rt-id')
|
||||||
|
@ -202,10 +204,10 @@ class OutgoingReport(BaseReport):
|
||||||
ticket = None
|
ticket = None
|
||||||
errmsg = error.args[0]
|
errmsg = error.args[0]
|
||||||
if ticket is None:
|
if ticket is None:
|
||||||
print("error: can't generate outgoings report for {}"
|
self.logger.error(
|
||||||
" because no RT ticket available: {}".format(
|
"can't generate outgoings report for %s because no RT ticket available: %s",
|
||||||
invoice, errmsg,
|
invoice, errmsg,
|
||||||
), file=self.err_file)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -379,11 +381,23 @@ metadata to match. A single ticket number is a shortcut for
|
||||||
args.search_terms = [SearchTerm.parse(s) for s in args.search]
|
args.search_terms = [SearchTerm.parse(s) for s in args.search]
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
def setup_logger(logger: logging.Logger, loglevel: int, stream: TextIO=sys.stderr) -> None:
|
||||||
|
formatter = logging.Formatter('%(name)s: %(levelname)s: %(message)s')
|
||||||
|
handler = logging.StreamHandler(stream)
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
logger.setLevel(loglevel)
|
||||||
|
|
||||||
def main(arglist: Optional[Sequence[str]]=None,
|
def main(arglist: Optional[Sequence[str]]=None,
|
||||||
stdout: TextIO=sys.stdout,
|
stdout: TextIO=sys.stdout,
|
||||||
stderr: TextIO=sys.stderr,
|
stderr: TextIO=sys.stderr,
|
||||||
config: Optional[configmod.Config]=None,
|
config: Optional[configmod.Config]=None,
|
||||||
|
logger: Optional[logging.Logger]=None,
|
||||||
) -> int:
|
) -> int:
|
||||||
|
if logger is None:
|
||||||
|
global _logger
|
||||||
|
_logger = logger = logging.getLogger('accrual-report')
|
||||||
|
setup_logger(_logger, logging.INFO, stderr)
|
||||||
returncode = 0
|
returncode = 0
|
||||||
args = parse_arguments(arglist)
|
args = parse_arguments(arglist)
|
||||||
if config is None:
|
if config is None:
|
||||||
|
@ -412,20 +426,17 @@ def main(arglist: Optional[Sequence[str]]=None,
|
||||||
if args.report_type is None:
|
if args.report_type is None:
|
||||||
args.report_type = ReportType.default_for(groups)
|
args.report_type = ReportType.default_for(groups)
|
||||||
if not groups:
|
if not groups:
|
||||||
print("warning: no matching entries found to report", file=stderr)
|
logger.warning("no matching entries found to report")
|
||||||
returncode |= ReturnFlag.NOTHING_TO_REPORT
|
returncode |= ReturnFlag.NOTHING_TO_REPORT
|
||||||
report: Optional[BaseReport] = None
|
report: Optional[BaseReport] = None
|
||||||
if args.report_type is ReportType.OUTGOING:
|
if args.report_type is ReportType.OUTGOING:
|
||||||
rt_client = config.rt_client()
|
rt_client = config.rt_client()
|
||||||
if rt_client is None:
|
if rt_client is None:
|
||||||
print(
|
logger.error("unable to generate outgoing report: RT client is required")
|
||||||
"error: unable to generate outgoing report: RT client is required",
|
|
||||||
file=stderr,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
report = OutgoingReport(rt_client, stdout, stderr)
|
report = OutgoingReport(rt_client, stdout)
|
||||||
else:
|
else:
|
||||||
report = args.report_type.value(stdout, stderr)
|
report = args.report_type.value(stdout)
|
||||||
if report is None:
|
if report is None:
|
||||||
returncode |= ReturnFlag.REPORT_ERRORS
|
returncode |= ReturnFlag.REPORT_ERRORS
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -259,10 +259,9 @@ def run_outgoing(invoice, postings, rt_client=None):
|
||||||
if not isinstance(postings, core.RelatedPostings):
|
if not isinstance(postings, core.RelatedPostings):
|
||||||
postings = relate_accruals_by_meta(postings, invoice)
|
postings = relate_accruals_by_meta(postings, invoice)
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
errors = io.StringIO()
|
report = accrual.OutgoingReport(rt_client, output)
|
||||||
report = accrual.OutgoingReport(rt_client, output, errors)
|
|
||||||
report.run({invoice: postings})
|
report.run({invoice: postings})
|
||||||
return output, errors
|
return output
|
||||||
|
|
||||||
@pytest.mark.parametrize('invoice,expected', [
|
@pytest.mark.parametrize('invoice,expected', [
|
||||||
('rt:505/5050', "Zero balance outstanding since 2020-05-05"),
|
('rt:505/5050', "Zero balance outstanding since 2020-05-05"),
|
||||||
|
@ -270,22 +269,21 @@ def run_outgoing(invoice, postings, rt_client=None):
|
||||||
('rt:510/6100', "-280.00 USD outstanding since 2020-06-10"),
|
('rt:510/6100', "-280.00 USD outstanding since 2020-06-10"),
|
||||||
('rt://ticket/515/attachments/5150', "1,500.00 USD outstanding since 2020-05-15",),
|
('rt://ticket/515/attachments/5150', "1,500.00 USD outstanding since 2020-05-15",),
|
||||||
])
|
])
|
||||||
def test_balance_report(accrual_postings, invoice, expected):
|
def test_balance_report(accrual_postings, invoice, expected, caplog):
|
||||||
related = relate_accruals_by_meta(accrual_postings, invoice)
|
related = relate_accruals_by_meta(accrual_postings, invoice)
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
errors = io.StringIO()
|
report = accrual.BalanceReport(output)
|
||||||
report = accrual.BalanceReport(output, errors)
|
|
||||||
report.run({invoice: related})
|
report.run({invoice: related})
|
||||||
assert not errors.getvalue()
|
assert not caplog.records
|
||||||
check_output(output, [invoice, expected])
|
check_output(output, [invoice, expected])
|
||||||
|
|
||||||
def test_outgoing_report(accrual_postings):
|
def test_outgoing_report(accrual_postings, caplog):
|
||||||
invoice = 'rt:510/6100'
|
invoice = 'rt:510/6100'
|
||||||
output, errors = run_outgoing(invoice, accrual_postings)
|
output = run_outgoing(invoice, accrual_postings)
|
||||||
rt_url = RTClient.DEFAULT_URL[:-9]
|
rt_url = RTClient.DEFAULT_URL[:-9]
|
||||||
rt_id_url = rf'\b{re.escape(f"{rt_url}Ticket/Display.html?id=510")}\b'
|
rt_id_url = rf'\b{re.escape(f"{rt_url}Ticket/Display.html?id=510")}\b'
|
||||||
contract_url = rf'\b{re.escape(f"{rt_url}Ticket/Attachment/4000/4000/contract.pdf")}\b'
|
contract_url = rf'\b{re.escape(f"{rt_url}Ticket/Attachment/4000/4000/contract.pdf")}\b'
|
||||||
print(output.getvalue())
|
assert not caplog.records
|
||||||
check_output(output, [
|
check_output(output, [
|
||||||
r'^PAYMENT FOR APPROVAL:$',
|
r'^PAYMENT FOR APPROVAL:$',
|
||||||
r'^REQUESTOR: Mx\. 510 <mx510@example\.org>$',
|
r'^REQUESTOR: Mx\. 510 <mx510@example\.org>$',
|
||||||
|
@ -304,11 +302,11 @@ def test_outgoing_report(accrual_postings):
|
||||||
r'^\s+Expenses:FilingFees\s+60\.00 USD$',
|
r'^\s+Expenses:FilingFees\s+60\.00 USD$',
|
||||||
])
|
])
|
||||||
|
|
||||||
def test_outgoing_report_custom_field_fallbacks(accrual_postings):
|
def test_outgoing_report_custom_field_fallbacks(accrual_postings, caplog):
|
||||||
invoice = 'rt:510/6100'
|
invoice = 'rt:510/6100'
|
||||||
rt_client = RTClient(want_cfs=False)
|
rt_client = RTClient(want_cfs=False)
|
||||||
output, errors = run_outgoing(invoice, accrual_postings, rt_client)
|
output = run_outgoing(invoice, accrual_postings, rt_client)
|
||||||
assert not errors.getvalue()
|
assert not caplog.records
|
||||||
check_output(output, [
|
check_output(output, [
|
||||||
r'^PAYMENT FOR APPROVAL:$',
|
r'^PAYMENT FOR APPROVAL:$',
|
||||||
r'^REQUESTOR: <mx510@example\.org>$',
|
r'^REQUESTOR: <mx510@example\.org>$',
|
||||||
|
@ -316,10 +314,10 @@ def test_outgoing_report_custom_field_fallbacks(accrual_postings):
|
||||||
r'^PAYMENT METHOD:\s*$',
|
r'^PAYMENT METHOD:\s*$',
|
||||||
])
|
])
|
||||||
|
|
||||||
def test_outgoing_report_fx_amounts(accrual_postings):
|
def test_outgoing_report_fx_amounts(accrual_postings, caplog):
|
||||||
invoice = 'rt:520/5200'
|
invoice = 'rt:520/5200'
|
||||||
output, errors = run_outgoing(invoice, accrual_postings)
|
output = run_outgoing(invoice, accrual_postings)
|
||||||
assert not errors.getvalue()
|
assert not caplog.records
|
||||||
check_output(output, [
|
check_output(output, [
|
||||||
r'^PAYMENT FOR APPROVAL:$',
|
r'^PAYMENT FOR APPROVAL:$',
|
||||||
r'^REQUESTOR: Mx\. 520 <mx520@example\.org>$',
|
r'^REQUESTOR: Mx\. 520 <mx520@example\.org>$',
|
||||||
|
@ -417,7 +415,7 @@ def test_main_no_books():
|
||||||
])
|
])
|
||||||
def test_main_no_matches(arglist):
|
def test_main_no_matches(arglist):
|
||||||
check_main_fails(arglist, None, 8, [
|
check_main_fails(arglist, None, 8, [
|
||||||
r'^warning: no matching entries found to report$',
|
r': WARNING: no matching entries found to report$',
|
||||||
])
|
])
|
||||||
|
|
||||||
def test_main_no_rt():
|
def test_main_no_rt():
|
||||||
|
@ -425,5 +423,5 @@ def test_main_no_rt():
|
||||||
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, 4, [
|
||||||
r'^error: unable to generate outgoing report: RT client is required\b',
|
r': ERROR: unable to generate outgoing report: RT client is required\b',
|
||||||
])
|
])
|
||||||
|
|
Loading…
Reference in a new issue