hooks.ledger_entry: New hook to output Ledger entries.
This is roughly the smallest diff necessary to move output to a hook. There's a lot of code reorganization that should still happen to bring it better in line with this new structure.
This commit is contained in:
parent
d2f8772e08
commit
76f2707aac
6 changed files with 100 additions and 28 deletions
|
@ -22,17 +22,7 @@ class FileImporter:
|
||||||
for importer in self.importers:
|
for importer in self.importers:
|
||||||
in_file.seek(0)
|
in_file.seek(0)
|
||||||
if importer.can_import(in_file):
|
if importer.can_import(in_file):
|
||||||
try:
|
importers.append(importer)
|
||||||
template = self.config.get_template(importer.TEMPLATE_KEY)
|
|
||||||
except errors.UserInputConfigurationError as error:
|
|
||||||
if error.strerror.startswith('template not defined '):
|
|
||||||
have_template = False
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
have_template = not template.is_empty()
|
|
||||||
if have_template:
|
|
||||||
importers.append((importer, template))
|
|
||||||
if not importers:
|
if not importers:
|
||||||
raise errors.UserInputFileError("no importers available", in_file.name)
|
raise errors.UserInputFileError("no importers available", in_file.name)
|
||||||
source_vars = {
|
source_vars = {
|
||||||
|
@ -43,21 +33,19 @@ class FileImporter:
|
||||||
'source_path': in_path.as_posix(),
|
'source_path': in_path.as_posix(),
|
||||||
'source_stem': in_path.stem,
|
'source_stem': in_path.stem,
|
||||||
}
|
}
|
||||||
with self.config.open_output_file() as out_file:
|
for importer in importers:
|
||||||
for importer, template in importers:
|
in_file.seek(0)
|
||||||
in_file.seek(0)
|
source_vars['template'] = importer.TEMPLATE_KEY
|
||||||
for entry_data in importer(in_file):
|
for entry_data in importer(in_file):
|
||||||
for hook in self.hooks:
|
entry_data = collections.ChainMap(entry_data, source_vars)
|
||||||
hook_retval = hook.run(entry_data)
|
for hook in self.hooks:
|
||||||
if hook_retval is None:
|
hook_retval = hook.run(entry_data)
|
||||||
pass
|
if hook_retval is None:
|
||||||
elif hook_retval is False:
|
pass
|
||||||
break
|
elif hook_retval is False:
|
||||||
else:
|
break
|
||||||
entry_data = hook_retval
|
|
||||||
else:
|
else:
|
||||||
render_vars = collections.ChainMap(entry_data, source_vars)
|
entry_data = hook_retval
|
||||||
print(template.render(render_vars), file=out_file, end='')
|
|
||||||
|
|
||||||
def import_path(self, in_path):
|
def import_path(self, in_path):
|
||||||
if in_path is None:
|
if in_path is None:
|
||||||
|
|
|
@ -19,6 +19,8 @@ HOOK_KINDS = enum.Enum('HOOK_KINDS', [
|
||||||
# DATA_FILTER hooks make a decision about whether or not to proceed with
|
# DATA_FILTER hooks make a decision about whether or not to proceed with
|
||||||
# processing the entry.
|
# processing the entry.
|
||||||
'DATA_FILTER',
|
'DATA_FILTER',
|
||||||
|
# OUTPUT hooks run last, sending the data somewhere else.
|
||||||
|
'OUTPUT',
|
||||||
])
|
])
|
||||||
|
|
||||||
def load_all():
|
def load_all():
|
||||||
|
|
23
import2ledger/hooks/ledger_entry.py
Normal file
23
import2ledger/hooks/ledger_entry.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from . import HOOK_KINDS
|
||||||
|
|
||||||
|
from .. import errors
|
||||||
|
|
||||||
|
class LedgerEntryHook:
|
||||||
|
KIND = HOOK_KINDS.OUTPUT
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def run(self, entry_data):
|
||||||
|
try:
|
||||||
|
template = self.config.get_template(entry_data['template'])
|
||||||
|
except errors.UserInputConfigurationError as error:
|
||||||
|
if error.strerror.startswith('template not defined '):
|
||||||
|
have_template = False
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
have_template = not template.is_empty()
|
||||||
|
if have_template:
|
||||||
|
with self.config.open_output_file() as out_file:
|
||||||
|
print(template.render(entry_data), file=out_file, end='')
|
|
@ -40,3 +40,8 @@ template =
|
||||||
; :NonItem:
|
; :NonItem:
|
||||||
Income:Sales -{item_sales}
|
Income:Sales -{item_sales}
|
||||||
; :Item:
|
; :Item:
|
||||||
|
|
||||||
|
[Empty]
|
||||||
|
template =
|
||||||
|
|
||||||
|
[Nonexistent]
|
||||||
|
|
|
@ -5,13 +5,19 @@ import itertools
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from import2ledger import hooks
|
from import2ledger import hooks
|
||||||
from import2ledger.hooks import add_entity, default_date, filter_by_date
|
from import2ledger.hooks import add_entity, default_date, filter_by_date, ledger_entry
|
||||||
|
|
||||||
def test_load_all():
|
def test_load_all():
|
||||||
all_hooks = list(hooks.load_all())
|
all_hooks = list(hooks.load_all())
|
||||||
positions = {hook: index for index, hook in enumerate(all_hooks)}
|
positions = {hook: index for index, hook in enumerate(all_hooks)}
|
||||||
assert positions[default_date.DefaultDateHook] < positions[add_entity.AddEntityHook]
|
expected_order = [
|
||||||
assert positions[add_entity.AddEntityHook] < positions[filter_by_date.FilterByDateHook]
|
default_date.DefaultDateHook,
|
||||||
|
add_entity.AddEntityHook,
|
||||||
|
filter_by_date.FilterByDateHook,
|
||||||
|
ledger_entry.LedgerEntryHook,
|
||||||
|
]
|
||||||
|
actual_order = list(sorted(expected_order, key=positions.__getitem__))
|
||||||
|
assert actual_order == expected_order
|
||||||
|
|
||||||
@pytest.mark.parametrize('in_key,payee,out_key,expected', [
|
@pytest.mark.parametrize('in_key,payee,out_key,expected', [
|
||||||
('payee', 'Alex Smith', 'entity', 'Smith-Alex'),
|
('payee', 'Alex Smith', 'entity', 'Smith-Alex'),
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import collections
|
import collections
|
||||||
import configparser
|
import configparser
|
||||||
|
import contextlib
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
|
import io
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from import2ledger import errors, template
|
from import2ledger import errors, template
|
||||||
|
from import2ledger.hooks import ledger_entry
|
||||||
|
|
||||||
from . import DATA_DIR, normalize_whitespace
|
from . import DATA_DIR, normalize_whitespace
|
||||||
|
|
||||||
|
@ -199,3 +202,48 @@ def test_line1_not_custom_payee():
|
||||||
def test_bad_amount_expression(amount_expr):
|
def test_bad_amount_expression(amount_expr):
|
||||||
with pytest.raises(errors.UserInputError):
|
with pytest.raises(errors.UserInputError):
|
||||||
template.Template(" Income " + amount_expr)
|
template.Template(" Income " + amount_expr)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self):
|
||||||
|
self.stdout = io.StringIO()
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def open_output_file(self):
|
||||||
|
yield self.stdout
|
||||||
|
|
||||||
|
def get_template(self, key):
|
||||||
|
try:
|
||||||
|
return template_from(key)
|
||||||
|
except KeyError:
|
||||||
|
raise errors.UserInputConfigurationError(
|
||||||
|
"template not defined in test config", key)
|
||||||
|
|
||||||
|
|
||||||
|
def run_hook(entry_data):
|
||||||
|
hook_config = Config()
|
||||||
|
hook = ledger_entry.LedgerEntryHook(hook_config)
|
||||||
|
assert hook.run(entry_data) is None
|
||||||
|
stdout = hook_config.stdout.getvalue()
|
||||||
|
return normalize_whitespace(stdout).splitlines()
|
||||||
|
|
||||||
|
def hook_vars(template_key, payee, amount):
|
||||||
|
return template_vars(payee, amount, other_vars={'template': template_key})
|
||||||
|
|
||||||
|
def test_hook_renders_template():
|
||||||
|
entry_data = hook_vars('Simplest', 'BB', '0.99')
|
||||||
|
lines = run_hook(entry_data)
|
||||||
|
assert lines == [
|
||||||
|
"",
|
||||||
|
"2015/03/14 BB",
|
||||||
|
" Accrued:Accounts Receivable 0.99 USD",
|
||||||
|
" Income:Donations -0.99 USD",
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_hook_handles_empty_template():
|
||||||
|
entry_data = hook_vars('Empty', 'CC', 1)
|
||||||
|
assert not run_hook(entry_data)
|
||||||
|
|
||||||
|
def test_hook_handles_template_undefined():
|
||||||
|
entry_data = hook_vars('Nonexistent', 'DD', 1)
|
||||||
|
assert not run_hook(entry_data)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue