books: Loader.from_all() accepts a start FY argument.
This commit is contained in:
parent
8dbe807efb
commit
2bd3e8b462
3 changed files with 61 additions and 26 deletions
|
@ -135,16 +135,38 @@ class Loader:
|
||||||
errors.extend(new_errors)
|
errors.extend(new_errors)
|
||||||
return entries, errors, options_map
|
return entries, errors, options_map
|
||||||
|
|
||||||
def load_all(self) -> LoadResult:
|
def _path_year(self, path: Path) -> int:
|
||||||
"""Load all of the books
|
return int(path.stem)
|
||||||
|
|
||||||
This method loads all of the books. It finds the books by simply
|
def load_all(self, from_year: Optional[Year]=None) -> LoadResult:
|
||||||
globbing the filesystem. It still loads each fiscal year in sequence to
|
"""Load all of the books from a starting FY
|
||||||
provide the best cache utilization.
|
|
||||||
|
This method loads all of the books, starting from the fiscal year you
|
||||||
|
specify.
|
||||||
|
|
||||||
|
* Pass in a date to start from the FY for that date.
|
||||||
|
* Pass in an integer >= 1000 to start from that year.
|
||||||
|
* Pass in a smaller integer to start from an FY relative to today
|
||||||
|
(e.g., -2 starts two FYs before today).
|
||||||
|
* Pass is no argument to load all books from the first available FY.
|
||||||
|
|
||||||
|
This method finds books by globbing the filesystem. It still loads
|
||||||
|
each fiscal year in sequence to provide the best cache utilization.
|
||||||
"""
|
"""
|
||||||
path = Path(self.books_root, 'books')
|
path = Path(self.books_root, 'books')
|
||||||
fy_paths = list(path.glob('[1-9][0-9][0-9][0-9].beancount'))
|
fy_paths = list(path.glob('[1-9][0-9][0-9][0-9].beancount'))
|
||||||
fy_paths.sort(key=lambda path: int(path.stem))
|
fy_paths.sort(key=self._path_year)
|
||||||
|
if from_year is not None:
|
||||||
|
if not isinstance(from_year, int):
|
||||||
|
from_year = self.fiscal_year.for_date(from_year)
|
||||||
|
elif from_year < 1000:
|
||||||
|
from_year = self.fiscal_year.for_date() + from_year
|
||||||
|
for index, path in enumerate(fy_paths):
|
||||||
|
if self._path_year(path) >= from_year:
|
||||||
|
fy_paths = fy_paths[index:]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
fy_paths = []
|
||||||
return self._load_paths(iter(fy_paths))
|
return self._load_paths(iter(fy_paths))
|
||||||
|
|
||||||
def load_fy_range(self,
|
def load_fy_range(self,
|
||||||
|
@ -154,9 +176,8 @@ class Loader:
|
||||||
"""Load books for a range of fiscal years
|
"""Load books for a range of fiscal years
|
||||||
|
|
||||||
This method generates a range of fiscal years by calling
|
This method generates a range of fiscal years by calling
|
||||||
FiscalYear.range() with its first two arguments. It returns a string of
|
FiscalYear.range() with its arguments. It loads all the books within
|
||||||
Beancount directives to load the books from the first available fiscal
|
that range.
|
||||||
year through the end of the range.
|
|
||||||
"""
|
"""
|
||||||
fy_range = self.fiscal_year.range(from_fy, to_fy)
|
fy_range = self.fiscal_year.range(from_fy, to_fy)
|
||||||
fy_paths = self._iter_fy_books(fy_range)
|
fy_paths = self._iter_fy_books(fy_range)
|
||||||
|
|
|
@ -27,11 +27,13 @@ from . import testutil
|
||||||
from beancount.core import data as bc_data
|
from beancount.core import data as bc_data
|
||||||
from conservancy_beancount import books
|
from conservancy_beancount import books
|
||||||
|
|
||||||
|
FY_START_MONTH = 3
|
||||||
|
|
||||||
books_path = testutil.test_path('books')
|
books_path = testutil.test_path('books')
|
||||||
|
|
||||||
@pytest.fixture(scope='module')
|
@pytest.fixture(scope='module')
|
||||||
def conservancy_loader():
|
def conservancy_loader():
|
||||||
return books.Loader(books_path, books.FiscalYear(3))
|
return books.Loader(books_path, books.FiscalYear(FY_START_MONTH))
|
||||||
|
|
||||||
def check_openings(entries):
|
def check_openings(entries):
|
||||||
openings = collections.defaultdict(int)
|
openings = collections.defaultdict(int)
|
||||||
|
@ -41,18 +43,13 @@ def check_openings(entries):
|
||||||
for account, count in openings.items():
|
for account, count in openings.items():
|
||||||
assert count == 1, f"found {count} open directives for {account}"
|
assert count == 1, f"found {count} open directives for {account}"
|
||||||
|
|
||||||
def check_narrations(entries, expected):
|
def txn_dates(entries):
|
||||||
expected = iter(expected)
|
|
||||||
expected_next = next(expected)
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
if (isinstance(entry, bc_data.Transaction)
|
if isinstance(entry, bc_data.Transaction):
|
||||||
and entry.narration == expected_next):
|
yield entry.date
|
||||||
try:
|
|
||||||
expected_next = next(expected)
|
def txn_years(entries):
|
||||||
except StopIteration:
|
return frozenset(date.year for date in txn_dates(entries))
|
||||||
break
|
|
||||||
else:
|
|
||||||
assert None, f"{expected_next} not found in entry narrations"
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('from_fy,to_fy,expect_years', [
|
@pytest.mark.parametrize('from_fy,to_fy,expect_years', [
|
||||||
(2019, 2019, range(2019, 2020)),
|
(2019, 2019, range(2019, 2020)),
|
||||||
|
@ -70,7 +67,7 @@ def check_narrations(entries, expected):
|
||||||
def test_load_fy_range(conservancy_loader, from_fy, to_fy, expect_years):
|
def test_load_fy_range(conservancy_loader, from_fy, to_fy, expect_years):
|
||||||
entries, errors, options_map = conservancy_loader.load_fy_range(from_fy, to_fy)
|
entries, errors, options_map = conservancy_loader.load_fy_range(from_fy, to_fy)
|
||||||
assert not errors
|
assert not errors
|
||||||
check_narrations(entries, [f'{year} donation' for year in expect_years])
|
assert txn_years(entries).issuperset(expect_years)
|
||||||
|
|
||||||
def test_load_fy_range_does_not_duplicate_openings(conservancy_loader):
|
def test_load_fy_range_does_not_duplicate_openings(conservancy_loader):
|
||||||
entries, errors, options_map = conservancy_loader.load_fy_range(2010, 2030)
|
entries, errors, options_map = conservancy_loader.load_fy_range(2010, 2030)
|
||||||
|
@ -82,8 +79,25 @@ def test_load_fy_range_empty(conservancy_loader):
|
||||||
assert not entries
|
assert not entries
|
||||||
assert not options_map
|
assert not options_map
|
||||||
|
|
||||||
def test_load_all(conservancy_loader):
|
@pytest.mark.parametrize('from_year', [None, *range(2018, 2021)])
|
||||||
entries, errors, options_map = conservancy_loader.load_all()
|
def test_load_all(conservancy_loader, from_year):
|
||||||
|
entries, errors, options_map = conservancy_loader.load_all(from_year)
|
||||||
|
from_year = from_year or 2018
|
||||||
assert not errors
|
assert not errors
|
||||||
check_narrations(entries, [f'{year} donation' for year in range(2018, 2021)])
|
|
||||||
check_openings(entries)
|
check_openings(entries)
|
||||||
|
assert txn_years(entries).issuperset(range(from_year or 2018, 2021))
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('from_date', [
|
||||||
|
date(2019, 2, 1),
|
||||||
|
date(2019, 9, 15),
|
||||||
|
date(2020, 1, 20),
|
||||||
|
date(2020, 5, 31),
|
||||||
|
])
|
||||||
|
def test_load_all_from_date(conservancy_loader, from_date):
|
||||||
|
from_year = from_date.year
|
||||||
|
if from_date.month < FY_START_MONTH:
|
||||||
|
from_year -= 1
|
||||||
|
entries, errors, options_map = conservancy_loader.load_all(from_date)
|
||||||
|
assert not errors
|
||||||
|
check_openings(entries)
|
||||||
|
assert txn_years(entries).issuperset(range(from_year, 2021))
|
||||||
|
|
|
@ -217,7 +217,7 @@ class TestBooksLoader(books.Loader):
|
||||||
def __init__(self, source):
|
def __init__(self, source):
|
||||||
self.source = source
|
self.source = source
|
||||||
|
|
||||||
def load_all(self):
|
def load_all(self, from_year=None):
|
||||||
return bc_loader.load_file(self.source)
|
return bc_loader.load_file(self.source)
|
||||||
|
|
||||||
def load_fy_range(self, from_fy, to_fy=None):
|
def load_fy_range(self, from_fy, to_fy=None):
|
||||||
|
|
Loading…
Reference in a new issue