From 3a3afb79786b0439820ac5245d7fe28368d53f0c Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Sun, 17 May 2020 14:01:40 -0400 Subject: [PATCH] historical: Add Beancount output format. --- oxrlib/commands/historical.py | 23 +++++++++++++++++++ oxrlib/config.py | 5 ++-- tests/test_historical.py | 43 +++++++++++++++++++++++++---------- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/oxrlib/commands/historical.py b/oxrlib/commands/historical.py index 854eba7..3dee51f 100644 --- a/oxrlib/commands/historical.py +++ b/oxrlib/commands/historical.py @@ -172,9 +172,32 @@ class LedgerFormatter(Formatter): ) +class BeancountFormatter(LedgerFormatter): + COST_FMT = '{{{}}}' + + def __init__(self, cost_rates, price_rates=None, + signed_currencies=(), base_fmt='###0.###', + rate_precision=5, denomination=None): + super().__init__( + cost_rates, + price_rates, + (), + base_fmt.replace(',', ''), + rate_precision, + denomination, + ) + + def price_rate(self, from_amt, from_curr, to_curr): + if self.price_rates is None: + return None + else: + return self.price_rates.convert(from_amt, from_curr, to_curr) + + class Formats(enum.Enum): RAW = Formatter LEDGER = LedgerFormatter + BEANCOUNT = BeancountFormatter @classmethod def from_arg(cls, s): diff --git a/oxrlib/config.py b/oxrlib/config.py index 5110ab1..c5b7df6 100644 --- a/oxrlib/config.py +++ b/oxrlib/config.py @@ -110,8 +110,9 @@ class Configuration: hist_parser.add_argument( '--output-format', type=historical.Formats.from_arg, - choices=[fmt.name.lower() for fmt in historical.Formats], - help="Output format. Choices are %(choices)s. Default `raw`.", + help="Output format." + " Choices are `raw`, `ledger`, `beancount`." + " Default `raw`.", ) # --ledger and --no-ledger predate --output-format. hist_parser.add_argument( diff --git a/tests/test_historical.py b/tests/test_historical.py index 7e935f4..2d019e9 100644 --- a/tests/test_historical.py +++ b/tests/test_historical.py @@ -40,6 +40,7 @@ class FakeConfig: output = pytest.fixture(lambda: io.StringIO()) parametrize_format = pytest.mark.parametrize('output_format', [ oxrhist.Formats.LEDGER, + oxrhist.Formats.BEANCOUNT, ]) @pytest.fixture(scope='module') @@ -85,20 +86,38 @@ def lines_from_run(config, output): def check_fx_amount(config, lines, amount, cost, fx_code, fx_sign=None, price=None): if price is None: price = cost + rate_fmt = f'{{}} {re.escape(fx_code)}' cost = re.escape(cost) + r'\d*' price = re.escape(price) + r'\d*' - if fx_sign is not None and fx_code in config.args.signed_currencies: - rate_fmt = f'{re.escape(fx_sign)}{{}}' + if config.args.output_format is oxrhist.Formats.LEDGER: + if fx_sign is not None and fx_code in config.args.signed_currencies: + rate_fmt = f'{re.escape(fx_sign)}{{}}' + cost_re = '{{={}}}'.format(rate_fmt.format(cost)) + price_re = ' @ {}'.format(rate_fmt.format(price)) else: - rate_fmt = f'{{}} {re.escape(fx_code)}' - pattern = r'^{} {{={}}} @ {}$'.format( - re.escape(amount), - rate_fmt.format(cost), - rate_fmt.format(price), - ) + amount = amount.replace(',', '') + cost_re = '{{{}}}'.format(rate_fmt.format(cost)) + if config.args.from_date is None: + price_re = '' + else: + price_re = ' @ {}'.format(rate_fmt.format(price)) + pattern = r'^{} {}{}$'.format(re.escape(amount), cost_re, price_re) line = next(lines, "") assert re.match(pattern, line) +def check_nonfx_amount(config, lines, amount, code=None, sign=None): + if config.args.output_format is oxrhist.Formats.LEDGER: + if code is None: + code = 'USD' + sign = '$' + if code in config.args.signed_currencies and sign is not None: + expected = f'{sign}{amount}\n' + else: + expected = f'{amount} {code}\n' + else: + expected = f'{amount.replace(",", "")} {code or "USD"}\n' + assert next(lines, "") == expected + def test_rate_list(single_responder, output, any_date): config = build_config(single_responder, any_date) lines = lines_from_run(config, output) @@ -144,7 +163,7 @@ def test_ledger_conversion(single_responder, output, any_date, output_format): amount=300, output_format=output_format) lines = lines_from_run(config, output) check_fx_amount(config, lines, '300 ALL', '0.00691', 'USD', '$') - assert next(lines) == '$2.08\n' + check_nonfx_amount(config, lines, '2.08') assert next(lines, None) is None @parametrize_format @@ -173,7 +192,7 @@ def test_redundant_denomination(single_responder, output, any_date, output_forma output_format=output_format, denomination='USD') lines = lines_from_run(config, output) check_fx_amount(config, lines, '10.00 ANG', '0.558', 'USD', '$') - assert next(lines) == '$5.59\n' + check_nonfx_amount(config, lines, '5.59') assert next(lines, None) is None @parametrize_format @@ -182,7 +201,7 @@ def test_from_denomination(single_responder, output, any_date, output_format): from_currency='USD', to_currency='ALL', amount=10, output_format=output_format, denomination='USD') lines = lines_from_run(config, output) - assert next(lines) == '$10.00\n' + check_nonfx_amount(config, lines, '10.00') check_fx_amount(config, lines, '1,445 ALL', '0.00691', 'USD', '$') assert next(lines, None) is None @@ -197,7 +216,7 @@ def test_rate_precision_added_as_needed(single_responder, output, any_date, outp # Make sure the rate is specified with enough precision to get the # correct conversion amount. check_fx_amount(config, lines, '63,805.00 RUB', '0.0175204', 'USD', '$') - assert next(lines) == '$1,117.89\n' + check_nonfx_amount(config, lines, '1,117.89') assert next(lines, None) is None @parametrize_format