import2ledger: Support only importing entries in a date range.
This commit is contained in:
parent
b37575eabc
commit
ab9c65d20d
6 changed files with 127 additions and 14 deletions
|
@ -34,6 +34,9 @@ class FileImporter:
|
|||
for entry_data in importer(in_file):
|
||||
for hook in self.hooks:
|
||||
hook.run(entry_data)
|
||||
if not entry_data:
|
||||
break
|
||||
else:
|
||||
print(template.render(**entry_data), file=out_file, end='')
|
||||
|
||||
def import_path(self, in_path):
|
||||
|
|
|
@ -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)
|
||||
|
|
12
import2ledger/hooks/filter_by_date.py
Normal file
12
import2ledger/hooks/filter_by_date.py
Normal 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()
|
|
@ -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)),
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue