books: Start Loader class.
This commit is contained in:
parent
adf402442b
commit
855c1c2bf0
2 changed files with 152 additions and 0 deletions
|
@ -16,13 +16,18 @@
|
|||
|
||||
import datetime
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from typing import (
|
||||
Iterable,
|
||||
Mapping,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Union,
|
||||
)
|
||||
|
||||
PathLike = Union[str, Path]
|
||||
PluginsSpec = Mapping[str, Optional[str]]
|
||||
Year = Union[int, datetime.date]
|
||||
|
||||
class FiscalYear(NamedTuple):
|
||||
|
@ -71,3 +76,74 @@ class FiscalYear(NamedTuple):
|
|||
elif from_fy < 1000:
|
||||
from_fy, to_fy = to_fy, from_fy + to_fy
|
||||
return range(from_fy, to_fy + 1)
|
||||
|
||||
|
||||
class Loader:
|
||||
"""Load Beancount books organized by fiscal year"""
|
||||
|
||||
DEFAULT_PLUGINS: PluginsSpec = {
|
||||
'conservancy_beancount.plugin': None,
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
books_root: Path,
|
||||
fiscal_year: FiscalYear,
|
||||
plugins: Optional[PluginsSpec]=None,
|
||||
) -> None:
|
||||
"""Set up a books loader
|
||||
|
||||
Arguments:
|
||||
* books_root: A Path to a Beancount books checkout.
|
||||
* fiscal_year: A FiscalYear object, used to determine what books to
|
||||
load for a given date range.
|
||||
* plugins: A mapping that specifies what plugins should be loaded
|
||||
before any books. The keys are plugin names, and the values are the
|
||||
configuration parameters string to follow. A value of None means the
|
||||
plugin takes no configuration string. By default, the loader loads
|
||||
conservancy_beancount.plugin.
|
||||
"""
|
||||
if plugins is None:
|
||||
plugins = self.DEFAULT_PLUGINS
|
||||
self.books_root = books_root
|
||||
self.fiscal_year = fiscal_year
|
||||
self.plugins = dict(plugins)
|
||||
|
||||
def _format_include(self, year: int, subdir: PathLike='') -> str:
|
||||
file_path = Path(self.books_root, subdir, f'{year}.beancount')
|
||||
return f'include "{file_path}"'
|
||||
|
||||
def _format_plugin(self, name: str, optstring: Optional[str]=None) -> str:
|
||||
if optstring is None:
|
||||
return f'plugin "{name}"'
|
||||
else:
|
||||
return f'plugin "{name}" "{optstring}"'
|
||||
|
||||
def fy_range_string(self,
|
||||
from_fy: Year,
|
||||
to_fy: Optional[Year]=None,
|
||||
plugins: Optional[PluginsSpec]=None,
|
||||
) -> str:
|
||||
"""Return a string to load books for a range of fiscal years
|
||||
|
||||
This method generates a range of fiscal years by calling
|
||||
FiscalYear.range() with its first two arguments. It returns a string of
|
||||
Beancount directives to load all plugins and Beancount files for that
|
||||
range of fiscal years, suitable for passing to
|
||||
beancount.loader.load_string().
|
||||
|
||||
You can specify what plugins to load with the plugins argument. If not
|
||||
specified, the string loads the plugins specified for this instance.
|
||||
See the __init__ docstring for details.
|
||||
"""
|
||||
if plugins is None:
|
||||
plugins = self.plugins
|
||||
years = iter(self.fiscal_year.range(from_fy, to_fy))
|
||||
try:
|
||||
books_start = self._format_include(next(years), 'books')
|
||||
except StopIteration:
|
||||
return ''
|
||||
return '\n'.join([
|
||||
*(self._format_plugin(name, opts) for name, opts in plugins.items()),
|
||||
books_start,
|
||||
*(self._format_include(year) for year in years),
|
||||
])
|
||||
|
|
76
tests/test_books_loader.py
Normal file
76
tests/test_books_loader.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
"""test_books_loader - Unit tests for books Loader class"""
|
||||
# Copyright © 2020 Brett Smith
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from . import testutil
|
||||
|
||||
from conservancy_beancount import books
|
||||
|
||||
books_path = testutil.test_path('booksroot')
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def conservancy_loader():
|
||||
return books.Loader(books_path, books.FiscalYear(3))
|
||||
|
||||
def format_include(year, subdir=''):
|
||||
path = Path(books_path, subdir, f'{year}.beancount')
|
||||
return f'include "{path}"'
|
||||
|
||||
def format_plugin(name, optstring=None):
|
||||
if optstring is None:
|
||||
return f'plugin "{name}"'
|
||||
else:
|
||||
return f'plugin "{name}" "{optstring}"'
|
||||
|
||||
def expect_string(years, plugins={'conservancy_beancount.plugin': None}):
|
||||
years = iter(years)
|
||||
year1_s = format_include(next(years), 'books')
|
||||
return '\n'.join([
|
||||
*(format_plugin(name, opts) for name, opts in plugins.items()),
|
||||
year1_s,
|
||||
*(format_include(year) for year in years),
|
||||
])
|
||||
|
||||
@pytest.mark.parametrize('range_start,range_stop,expect_years', [
|
||||
(2019, 2020, [2019, 2020]),
|
||||
(-1, 2020, [2019, 2020]),
|
||||
(date(2019, 1, 1), date(2020, 6, 1), range(2018, 2021)),
|
||||
(-1, date(2020, 2, 1), [2018, 2019]),
|
||||
])
|
||||
def test_fy_range_string(conservancy_loader, range_start, range_stop, expect_years):
|
||||
expected = expect_string(expect_years)
|
||||
assert conservancy_loader.fy_range_string(range_start, range_stop) == expected
|
||||
|
||||
@pytest.mark.parametrize('year_offset', range(-3, 1))
|
||||
def test_fy_range_string_one_offset(conservancy_loader, year_offset):
|
||||
this_year = date.today().year
|
||||
expected = expect_string(range(this_year + year_offset, this_year + 1))
|
||||
assert conservancy_loader.fy_range_string(year_offset) == expected
|
||||
|
||||
@pytest.mark.parametrize('plugins', [
|
||||
{},
|
||||
{'conservancy_beancount.plugin': '-all'},
|
||||
])
|
||||
def test_fy_range_string_plugins_override(conservancy_loader, plugins):
|
||||
expected = expect_string([2019, 2020], plugins)
|
||||
assert conservancy_loader.fy_range_string(2019, 2020, plugins) == expected
|
||||
|
||||
def test_fy_range_string_empty_range(conservancy_loader):
|
||||
assert conservancy_loader.fy_range_string(2020, 2019) == ''
|
Loading…
Reference in a new issue