Files
@ 43a2e1bec828
Branch filter:
Location: NPO-Accounting/conservancy_beancount/conservancy_beancount/books.py
43a2e1bec828
5.2 KiB
text/x-python
beancount_types: Add types related to loading the books.
These will help support loading methods in the books module.
These will help support loading methods in the books module.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | """books - Tools for loading the books"""
# 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/>.
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):
month: int = 3
day: int = 1
def for_date(self, date: Optional[datetime.date]=None) -> int:
if date is None:
date = datetime.date.today()
if (date.month, date.day) < self:
return date.year - 1
else:
return date.year
def range(self, from_fy: Year, to_fy: Optional[Year]=None) -> Iterable[int]:
"""Return a range of fiscal years
Both arguments can be either a year (represented as an integer) or a
date. Dates will be converted into a year by calling for_date() on
them.
If the first argument is negative or below 1000, it will be treated as
an offset. You'll get a range of fiscal years between the second
argument offset by this amount.
If the second argument is omitted, it defaults to the current fiscal
year.
Note that unlike normal Python ranges, these ranges include the final
fiscal year.
Examples:
range(2015) # Iterate all fiscal years from 2015 to today, inclusive
range(-1) # Iterate the previous fiscal year and current fiscal year
"""
if not isinstance(from_fy, int):
from_fy = self.for_date(from_fy)
if to_fy is None:
to_fy = self.for_date()
elif not isinstance(to_fy, int):
to_fy = self.for_date(to_fy - datetime.timedelta(days=1))
if from_fy < 1:
from_fy += to_fy
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),
])
|