import2ledger: Support only importing entries in a date range.

This commit is contained in:
Brett Smith 2017-10-22 16:10:17 -04:00
parent b37575eabc
commit ab9c65d20d
6 changed files with 127 additions and 14 deletions

View file

@ -34,7 +34,10 @@ class FileImporter:
for entry_data in importer(in_file):
for hook in self.hooks:
hook.run(entry_data)
print(template.render(**entry_data), file=out_file, end='')
if not entry_data:
break
else:
print(template.render(**entry_data), file=out_file, end='')
def import_path(self, in_path):
if in_path is None:

View file

@ -19,6 +19,7 @@ class Configuration:
TODAY = datetime.date.today()
CONFIG_DEFAULTS = {
'date_format': '%%Y/%%m/%%d',
'date_range': '-',
'loglevel': 'WARNING',
'output_path': '-',
'signed_currencies': ','.join(babel.numbers.get_territory_currencies(
@ -67,11 +68,6 @@ class Configuration:
description="These options take priority over settings in the "
"[DEFAULT] section of your config file, but not other sections.",
)
parser.add_argument(
'--loglevel', '-L', metavar='LEVEL',
choices=['debug', 'info', 'warning', 'error', 'critical'],
help="Log messages at this level and above. Default WARNING.",
)
out_args.add_argument(
'--date', '-d', metavar='DATE',
help="Date to use in Ledger entries when the source doesn't "
@ -82,6 +78,17 @@ class Configuration:
'--date-format', '-D', metavar='FORMAT',
help="Date format to use in Ledger entries",
)
out_args.add_argument(
'--date-range', metavar='DATE-DATE',
help="Only import entries in this date range, inclusive. "
"Write dates in your configured date format. "
"You can omit either side of the range.",
)
out_args.add_argument(
'--loglevel', '-L', metavar='LEVEL',
choices=['debug', 'info', 'warning', 'error', 'critical'],
help="Log messages at this level and above. Default WARNING.",
)
out_args.add_argument(
'--output-path', '-O', metavar='PATH',
help="Path of file to append entries to, or '-' for stdout (default).",
@ -125,14 +132,36 @@ class Configuration:
except KeyError:
return default
def _parse_date_range(self, section_name):
section = self.conffile[section_name]
range_s = section['date_range']
date_fmt = section['date_format']
if not range_s:
range_s = '-'
if range_s.startswith('-'):
start_s = ''
end_s = range_s[1:]
elif range_s.endswith('-'):
start_s = range_s[:-1]
end_s = ''
else:
range_parts = range_s.split('-')
mid_index = len(range_parts) // 2
start_s = '-'.join(range_parts[:mid_index])
end_s = '-'.join(range_parts[mid_index:])
start_d = self._strpdate(start_s, date_fmt) if start_s else datetime.date.min
end_d = self._strpdate(end_s, date_fmt) if end_s else datetime.date.max
return range(start_d.toordinal(), end_d.toordinal() + 1)
def finalize(self):
default_secname = self.conffile.default_section
if self.args.use_config is None:
self.args.use_config = self.conffile.default_section
self.args.use_config = default_secname
elif not self.conffile.has_section(self.args.use_config):
self.error("section {!r} not found in config file".format(self.args.use_config))
self.args.input_paths = [self._s_to_path(s) for s in self.args.input_paths]
defaults = self.conffile[self.conffile.default_section]
defaults = self.conffile[default_secname]
for key in self.CONFIG_DEFAULTS:
value = getattr(self.args, key)
if value is None:
@ -140,7 +169,7 @@ class Configuration:
elif key == 'signed_currencies':
defaults[key] = ','.join(value)
else:
defaults[key] = value
defaults[key] = value.replace('%', '%%')
# We parse all the dates now to make sure they're valid.
if self.args.date is not None:
@ -152,7 +181,10 @@ class Configuration:
self.dates = {secname: self._parse_section_date(secname, default_date)
for secname in self.conffile}
self.dates[self.conffile.default_section] = default_date
self.dates[default_secname] = default_date
self.date_ranges = {secname: self._parse_date_range(secname)
for secname in self.conffile}
self.date_ranges[default_secname] = self._parse_date_range(default_secname)
@contextlib.contextmanager
def from_section(self, section_name):
@ -168,13 +200,19 @@ class Configuration:
section_name = self.args.use_config
return self.conffile[section_name]
def get_default_date(self, section_name=None):
def _get_from_dict(self, confdict, section_name=None):
if section_name is None:
section_name = self.args.use_config
try:
return self.dates[section_name]
return confdict[section_name]
except KeyError:
return self.dates[self.conffile.default_section]
return confdict[self.conffile.default_section]
def date_in_want_range(self, date, section_name=None):
return date.toordinal() in self._get_from_dict(self.date_ranges, section_name)
def get_default_date(self, section_name=None):
return self._get_from_dict(self.dates, section_name)
def get_loglevel(self, section_name=None):
section_config = self._get_section(section_name)

View file

@ -0,0 +1,12 @@
class FilterByDateHook:
def __init__(self, config):
self.config = config
def run(self, entry_data):
try:
date = entry_data['date']
except KeyError:
pass
else:
if not self.config.date_in_want_range(date):
entry_data.clear()

View file

@ -1,5 +1,6 @@
import contextlib
import datetime
import itertools
import logging
import os
import pathlib
@ -63,6 +64,22 @@ def test_output_path_from_section():
with config.from_section('Templates'):
assert config.get_output_path() == expected_path
@pytest.mark.parametrize('range_s,date_fmt', [
(range_s.replace('/', sep), sep.join(['%Y', '%m', '%d']))
for range_s, sep in itertools.product([
'-',
'2016/06/01-2016/06/30',
'2016/06/01-',
'-2016/06/30',
], '/-')
])
def test_date_in_want_range(range_s, date_fmt):
config = config_from_file(os.devnull, ['--date-range=' + range_s, '--date-format', date_fmt])
assert config.date_in_want_range(datetime.date(2016, 5, 31)) == range_s.startswith('-')
assert config.date_in_want_range(datetime.date(2016, 6, 1))
assert config.date_in_want_range(datetime.date(2016, 6, 30))
assert config.date_in_want_range(datetime.date(2016, 7, 1)) == range_s.endswith('-')
@pytest.mark.parametrize('arglist,expect_date', [
([], None),
(['-d', '2017-10-12'], datetime.date(2017, 10, 12)),

View file

@ -5,7 +5,7 @@ import itertools
import pytest
from import2ledger import hooks
from import2ledger.hooks import add_entity, default_date
from import2ledger.hooks import add_entity, default_date, filter_by_date
def test_load_all():
all_hooks = list(hooks.load_all())
@ -30,6 +30,36 @@ def test_add_entity(payee, expected):
assert data['entity'] == expected
class DateRangeConfig:
def __init__(self, start_date=None, end_date=None):
self.start_date = start_date
self.end_date = end_date
def date_in_want_range(self, date):
return (
((self.start_date is None) or (date >= self.start_date))
and ((self.end_date is None) or (date <= self.end_date))
)
@pytest.mark.parametrize('entry_date,start_date,end_date,allowed', [
(datetime.date(2016, 5, 10), datetime.date(2016, 1, 1), datetime.date(2016, 12, 31), True),
(datetime.date(2016, 1, 1), datetime.date(2016, 1, 1), datetime.date(2016, 12, 31), True),
(datetime.date(2016, 12, 31), datetime.date(2016, 1, 1), datetime.date(2016, 12, 31), True),
(datetime.date(2016, 1, 1), datetime.date(2016, 1, 1), None, True),
(datetime.date(2016, 12, 31), None, datetime.date(2016, 12, 31), True),
(datetime.date(1999, 1, 2), None, None, True),
(datetime.date(2016, 1, 25), datetime.date(2016, 2, 1), datetime.date(2016, 12, 31), False),
(datetime.date(2016, 12, 26), datetime.date(2016, 1, 1), datetime.date(2016, 11, 30), False),
(datetime.date(2016, 1, 31), datetime.date(2016, 2, 1), None, False),
(datetime.date(2016, 12, 1), None, datetime.date(2016, 11, 30), False),
])
def test_filter_by_date(entry_date, start_date, end_date, allowed):
entry_data = {'date': entry_date}
hook = filter_by_date.FilterByDateHook(DateRangeConfig(start_date, end_date))
hook.run(entry_data)
assert bool(entry_data) == allowed
class DefaultDateConfig:
ONE_DAY = datetime.timedelta(days=1)

View file

@ -49,3 +49,16 @@ def test_fees_import():
assert exitcode == 0
actual = entries2set(stdout)
assert actual == expected_entries('test_main_fees_import.ledger')
def test_date_range_import():
arglist = ARGLIST + [
'-c', 'One',
'--date-range', '2017/10/01-',
pathlib.Path(DATA_DIR, 'PatreonEarnings.csv').as_posix(),
]
exitcode, stdout, _ = run_main(arglist)
assert exitcode == 0
actual = entries2set(stdout)
expected = {entry for entry in expected_entries('test_main_fees_import.ledger')
if entry.startswith('2017/10/')}
assert actual == expected