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,7 +34,10 @@ class FileImporter:
|
||||||
for entry_data in importer(in_file):
|
for entry_data in importer(in_file):
|
||||||
for hook in self.hooks:
|
for hook in self.hooks:
|
||||||
hook.run(entry_data)
|
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):
|
def import_path(self, in_path):
|
||||||
if in_path is None:
|
if in_path is None:
|
||||||
|
|
|
@ -19,6 +19,7 @@ class Configuration:
|
||||||
TODAY = datetime.date.today()
|
TODAY = datetime.date.today()
|
||||||
CONFIG_DEFAULTS = {
|
CONFIG_DEFAULTS = {
|
||||||
'date_format': '%%Y/%%m/%%d',
|
'date_format': '%%Y/%%m/%%d',
|
||||||
|
'date_range': '-',
|
||||||
'loglevel': 'WARNING',
|
'loglevel': 'WARNING',
|
||||||
'output_path': '-',
|
'output_path': '-',
|
||||||
'signed_currencies': ','.join(babel.numbers.get_territory_currencies(
|
'signed_currencies': ','.join(babel.numbers.get_territory_currencies(
|
||||||
|
@ -67,11 +68,6 @@ class Configuration:
|
||||||
description="These options take priority over settings in the "
|
description="These options take priority over settings in the "
|
||||||
"[DEFAULT] section of your config file, but not other sections.",
|
"[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(
|
out_args.add_argument(
|
||||||
'--date', '-d', metavar='DATE',
|
'--date', '-d', metavar='DATE',
|
||||||
help="Date to use in Ledger entries when the source doesn't "
|
help="Date to use in Ledger entries when the source doesn't "
|
||||||
|
@ -82,6 +78,17 @@ class Configuration:
|
||||||
'--date-format', '-D', metavar='FORMAT',
|
'--date-format', '-D', metavar='FORMAT',
|
||||||
help="Date format to use in Ledger entries",
|
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(
|
out_args.add_argument(
|
||||||
'--output-path', '-O', metavar='PATH',
|
'--output-path', '-O', metavar='PATH',
|
||||||
help="Path of file to append entries to, or '-' for stdout (default).",
|
help="Path of file to append entries to, or '-' for stdout (default).",
|
||||||
|
@ -125,14 +132,36 @@ class Configuration:
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return default
|
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):
|
def finalize(self):
|
||||||
|
default_secname = self.conffile.default_section
|
||||||
if self.args.use_config is None:
|
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):
|
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.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]
|
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:
|
for key in self.CONFIG_DEFAULTS:
|
||||||
value = getattr(self.args, key)
|
value = getattr(self.args, key)
|
||||||
if value is None:
|
if value is None:
|
||||||
|
@ -140,7 +169,7 @@ class Configuration:
|
||||||
elif key == 'signed_currencies':
|
elif key == 'signed_currencies':
|
||||||
defaults[key] = ','.join(value)
|
defaults[key] = ','.join(value)
|
||||||
else:
|
else:
|
||||||
defaults[key] = value
|
defaults[key] = value.replace('%', '%%')
|
||||||
|
|
||||||
# We parse all the dates now to make sure they're valid.
|
# We parse all the dates now to make sure they're valid.
|
||||||
if self.args.date is not None:
|
if self.args.date is not None:
|
||||||
|
@ -152,7 +181,10 @@ class Configuration:
|
||||||
|
|
||||||
self.dates = {secname: self._parse_section_date(secname, default_date)
|
self.dates = {secname: self._parse_section_date(secname, default_date)
|
||||||
for secname in self.conffile}
|
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
|
@contextlib.contextmanager
|
||||||
def from_section(self, section_name):
|
def from_section(self, section_name):
|
||||||
|
@ -168,13 +200,19 @@ class Configuration:
|
||||||
section_name = self.args.use_config
|
section_name = self.args.use_config
|
||||||
return self.conffile[section_name]
|
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:
|
if section_name is None:
|
||||||
section_name = self.args.use_config
|
section_name = self.args.use_config
|
||||||
try:
|
try:
|
||||||
return self.dates[section_name]
|
return confdict[section_name]
|
||||||
except KeyError:
|
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):
|
def get_loglevel(self, section_name=None):
|
||||||
section_config = self._get_section(section_name)
|
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 contextlib
|
||||||
import datetime
|
import datetime
|
||||||
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
@ -63,6 +64,22 @@ def test_output_path_from_section():
|
||||||
with config.from_section('Templates'):
|
with config.from_section('Templates'):
|
||||||
assert config.get_output_path() == expected_path
|
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', [
|
@pytest.mark.parametrize('arglist,expect_date', [
|
||||||
([], None),
|
([], None),
|
||||||
(['-d', '2017-10-12'], datetime.date(2017, 10, 12)),
|
(['-d', '2017-10-12'], datetime.date(2017, 10, 12)),
|
||||||
|
|
|
@ -5,7 +5,7 @@ import itertools
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from import2ledger import hooks
|
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():
|
def test_load_all():
|
||||||
all_hooks = list(hooks.load_all())
|
all_hooks = list(hooks.load_all())
|
||||||
|
@ -30,6 +30,36 @@ def test_add_entity(payee, expected):
|
||||||
assert data['entity'] == 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:
|
class DefaultDateConfig:
|
||||||
ONE_DAY = datetime.timedelta(days=1)
|
ONE_DAY = datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
|
|
@ -49,3 +49,16 @@ def test_fees_import():
|
||||||
assert exitcode == 0
|
assert exitcode == 0
|
||||||
actual = entries2set(stdout)
|
actual = entries2set(stdout)
|
||||||
assert actual == expected_entries('test_main_fees_import.ledger')
|
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