Changeset - ca12496880d6
[Not reviewed]
0 10 0
Brett Smith - 4 years ago 2021-02-26 21:13:02
brettcsmith@brettcsmith.org
typing: Updates to pass type checking under mypy>=0.800.

Most of these account for the fact that mypy now reports that Hashable is
not an allowed return type for sort key functions.

That, plus the new ignore for the regression in config.py.
10 files changed with 22 insertions and 18 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/beancount_types.py
Show inline comments
...
 
@@ -7,48 +7,51 @@
 

	
 
import abc
 
import datetime
 

	
 
import beancount.core.data as bc_data
 

	
 
from typing import (
 
    TYPE_CHECKING,
 
    Any,
 
    Dict,
 
    FrozenSet,
 
    Iterable,
 
    List,
 
    Mapping,
 
    NamedTuple,
 
    Optional,
 
    Set,
 
    Tuple,
 
    Type,
 
    Union,
 
)
 

	
 
if TYPE_CHECKING:
 
    from . import errors as errormod
 
    from _typeshed import SupportsLessThan as Sortable
 
else:
 
    from typing import Hashable as Sortable
 

	
 
Account = bc_data.Account
 
Currency = bc_data.Currency
 
Meta = bc_data.Meta
 
MetaKey = str
 
MetaValue = Any
 
MetaValueEnum = str
 
Posting = bc_data.Posting
 

	
 
class Directive(NamedTuple):
 
    meta: Meta
 
    date: datetime.date
 

	
 

	
 
class Close(Directive):
 
    account: str
 

	
 

	
 
class Error(NamedTuple):
 
    source: Mapping[MetaKey, MetaValue]
 
    message: str
 
    entry: Optional[Directive]
 

	
 

	
conservancy_beancount/cliutil.py
Show inline comments
...
 
@@ -21,64 +21,64 @@ import pkg_resources
 
import re
 
import signal
 
import subprocess
 
import sys
 
import traceback
 
import types
 

	
 
from pathlib import Path
 

	
 
import rt.exceptions as rt_error
 
import yaml
 

	
 
from . import data
 
from . import errors
 
from . import filters
 
from . import rtutil
 

	
 
from typing import (
 
    cast,
 
    Any,
 
    BinaryIO,
 
    Callable,
 
    Container,
 
    Generic,
 
    Hashable,
 
    IO,
 
    Iterable,
 
    Iterator,
 
    List,
 
    NamedTuple,
 
    NoReturn,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
    Type,
 
    TypeVar,
 
    Union,
 
)
 
from .beancount_types import (
 
    MetaKey,
 
    Sortable,
 
)
 

	
 
ET = TypeVar('ET', bound=enum.Enum)
 
OutputFile = Union[int, IO]
 

	
 
CPU_COUNT = len(os.sched_getaffinity(0))
 
STDSTREAM_PATH = Path('-')
 
VERSION = pkg_resources.require(PKGNAME)[0].version
 

	
 
class EnumArgument(Generic[ET]):
 
    """Wrapper class to use an enum as argument values
 

	
 
    Use this class when the user can choose one of some arbitrary enum names
 
    as an argument. It will let user abbreviate and use any case, and will
 
    return the correct value as long as it's unambiguous. Typical usage
 
    looks like::
 

	
 
        enum_arg = EnumArgument(Enum)
 
        arg_parser.add_argument(
 
          '--choice',
 
          type=enum_arg.enum_type,  # or .value_type
 
          help=f"Choices are {enum_arg.choices_str()}",
 
 
        )
...
 
@@ -93,57 +93,57 @@ class EnumArgument(Generic[ET]):
 
        self.base = base
 

	
 
    def enum_type(self, arg: str) -> ET:
 
        """Return a single enum whose name matches the user argument"""
 
        regexp = re.compile(re.escape(arg), re.IGNORECASE)
 
        matches = frozenset(
 
            choice
 
            for name, choice in self.base.__members__.items()
 
            if regexp.match(name)
 
        )
 
        count = len(matches)
 
        if count == 1:
 
            return next(iter(matches))
 
        elif count:
 
            names = ', '.join(repr(choice.name) for choice in matches)
 
            raise ValueError(f"ambiguous argument {arg!r}: matches {names}")
 
        else:
 
            raise ValueError(f"unknown argument {arg!r}")
 

	
 
    def value_type(self, arg: str) -> Any:
 
        return self.enum_type(arg).value
 

	
 
    def choices_str(self, sep: str=', ', fmt: str='{!r}') -> str:
 
        """Return a user-formatted string of enum names"""
 
        sortkey: Callable[[ET], Hashable] = getattr(
 
        sortkey: Callable[[ET], Sortable] = getattr(
 
            self.base, '_choices_sortkey', self._choices_sortkey,
 
        )
 
        return sep.join(
 
            fmt.format(choice.name.lower())
 
            for choice in sorted(self.base, key=sortkey)
 
        )
 

	
 
    def _choices_sortkey(self, choice: ET) -> Hashable:
 
    def _choices_sortkey(self, choice: ET) -> Sortable:
 
        return choice.name
 

	
 

	
 
class ExceptHook:
 
    def __init__(self, logger: Optional[logging.Logger]=None) -> None:
 
        if logger is None:
 
            logger = logging.getLogger()
 
        self.logger = logger
 

	
 
    def __call__(self,
 
                 exc_type: Type[BaseException],
 
                 exc_value: BaseException,
 
                 exc_tb: types.TracebackType,
 
    ) -> NoReturn:
 
        error_type = type(exc_value).__name__
 
        msg = ": ".join(str(arg) for arg in exc_value.args)
 
        if isinstance(exc_value, KeyboardInterrupt):
 
            signal.signal(signal.SIGINT, signal.SIG_DFL)
 
            os.kill(0, signal.SIGINT)
 
            signal.pause()
 
        elif isinstance(exc_value, (
 
                rt_error.AuthorizationError,
 
                rt_error.NotAllowed,
 
        )):
...
 
@@ -186,49 +186,49 @@ class ExitCode(enum.IntEnum):
 
    # BSD exit codes commonly used
 
    NoConfiguration = os.EX_CONFIG
 
    NoConfig = NoConfiguration
 
    NoDataFiltered = os.EX_DATAERR
 
    NoDataLoaded = os.EX_NOINPUT
 
    OK = os.EX_OK
 
    Ok = OK
 
    RewriteRulesError = os.EX_DATAERR
 

	
 
    # Our own exit codes, working down from that range
 
    BeancountErrors = 63
 

	
 

	
 
class ExtendAction(argparse.Action):
 
    """argparse action to let a user build a list from a string
 

	
 
    This is a fancier version of argparse's built-in ``action='append'``.
 
    The user's input is turned into a list of strings, split by a regexp
 
    pattern you provide. Typical usage looks like::
 

	
 
        parser = argparse.ArgumentParser()
 
        parser.add_argument(
 
          '--option', ...,
 
          action=ExtendAction,
 
          const=regexp_pattern,  # default is r'\s*,\s*'
 
          const=regexp_pattern,  # default is '\\s*,\\s*'
 
          ...,
 
        )
 
    """
 
    DEFAULT_PATTERN = r'\s*,\s*'
 

	
 
    def __call__(self,
 
                 parser: argparse.ArgumentParser,
 
                 namespace: argparse.Namespace,
 
                 values: Union[Sequence[Any], str, None]=None,
 
                 option_string: Optional[str]=None,
 
    ) -> None:
 
        pattern: str = self.const or self.DEFAULT_PATTERN
 
        value: Optional[List[str]] = getattr(namespace, self.dest, None)
 
        if value is None:
 
            value = []
 
            setattr(namespace, self.dest, value)
 
        if values is None:
 
            values = []
 
        elif isinstance(values, str):
 
            values = [values]
 
        for s in values:
 
            value.extend(re.split(pattern, s))
 

	
 

	
...
 
@@ -237,49 +237,49 @@ class InfoAction(argparse.Action):
 
                 parser: argparse.ArgumentParser,
 
                 namespace: argparse.Namespace,
 
                 values: Union[Sequence[Any], str, None]=None,
 
                 option_string: Optional[str]=None,
 
    ) -> NoReturn:
 
        if isinstance(self.const, str):
 
            info = self.const
 
            exitcode = 0
 
        else:
 
            info, exitcode = self.const
 
        print(info)
 
        raise SystemExit(exitcode)
 

	
 

	
 
class LogLevel(enum.IntEnum):
 
    DEBUG = logging.DEBUG
 
    INFO = logging.INFO
 
    WARNING = logging.WARNING
 
    ERROR = logging.ERROR
 
    CRITICAL = logging.CRITICAL
 
    WARN = WARNING
 
    ERR = ERROR
 
    CRIT = CRITICAL
 

	
 
    def _choices_sortkey(self) -> Hashable:
 
    def _choices_sortkey(self) -> Sortable:
 
        return self.value
 

	
 

	
 
class SearchTerm(NamedTuple):
 
    """NamedTuple representing a user's metadata filter
 

	
 
    SearchTerm knows how to parse and store posting metadata filters provided
 
    by the user in `key=value` format. Reporting tools can use this to filter
 
    postings that match the user's criteria, to report on subsets of the books.
 

	
 
    Typical usage looks like::
 

	
 
      argument_parser.add_argument(
 
        'search_terms',
 
        type=SearchTerm.arg_parser(),
 
        …,
 
      )
 

	
 
      args = argument_parser.parse_args(…)
 
      for query in args.search_terms:
 
        postings = query.filter_postings(postings)
 
    """
 
    meta_key: MetaKey
 
    pattern: str
conservancy_beancount/config.py
Show inline comments
...
 
@@ -207,25 +207,26 @@ class Config:
 
            return None
 
        cache_dir_path = self.cache_dir_path()
 
        if cache_dir_path is None:
 
            cache_db = None
 
        else:
 
            cache_name = '{}@{}.sqlite3'.format(
 
                credentials.user,
 
                urlparse.quote(str(credentials.server), ''),
 
            )
 
            cache_path = cache_dir_path / cache_name
 
            try:
 
                cache_path.touch(0o600)
 
            except OSError:
 
                # RTLinkCache.setup() will handle the problem.
 
                pass
 
            cache_db = rtutil.RTLinkCache.setup(cache_path)
 
        return rtutil.RT(wrapper_client, cache_db)
 

	
 
    def rt_wrapper(self,
 
                  credentials: RTCredentials=None,
 
                  client: Type[rt.Rt]=rt.Rt,
 
    ) -> Optional[rtutil.RT]:
 
        if credentials is None:
 
            credentials = self.rt_credentials()
 
        return self._rt_wrapper(credentials, client)
 
        # type ignore for <https://github.com/python/typeshed/issues/4638>
 
        return self._rt_wrapper(credentials, client)  # type:ignore[arg-type]
conservancy_beancount/reconcile/paypal.py
Show inline comments
...
 
@@ -13,60 +13,62 @@ import datetime
 
import enum
 
import logging
 
import operator
 
import os
 
import sys
 
import unicodedata
 

	
 
from decimal import Decimal
 
from pathlib import Path
 

	
 
import odf.style  # type:ignore[import]
 
import odf.table  # type:ignore[import]
 

	
 
from beancount.parser import printer as bc_printer
 

	
 
from .. import books
 
from .. import cliutil
 
from .. import config as configmod
 
from .. import data
 
from ..ranges import DateRange
 
from ..reports import core
 

	
 
from typing import (
 
    Any,
 
    Hashable,
 
    Iterable,
 
    Iterator,
 
    List,
 
    Mapping,
 
    NamedTuple,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
    Tuple,
 
    Union,
 
)
 
from ..beancount_types import (
 
    Sortable,
 
)
 

	
 
PROGNAME = 'reconcile-paypal'
 
logger = logging.getLogger('conservancy_beancount.reconcile.paypal')
 

	
 
class ReconcileProblems(enum.IntFlag):
 
    NOT_IN_STATEMENT = enum.auto()
 
    DUP_IN_STATEMENT = enum.auto()
 
    AMOUNT_DIFF = enum.auto()
 
    MONTH_DIFF = enum.auto()
 
    DATE_DIFF = enum.auto()
 

	
 

	
 
class PayPalPosting(NamedTuple):
 
    filename: str
 
    lineno: int
 
    txn_id: str
 
    date: datetime.date
 
    amount: data.Amount
 
    description: str
 
    entity: Optional[str]
 

	
 
    @classmethod
 
    def from_books(cls, post: data.Posting) -> 'PayPalPosting':
 
        return cls(
...
 
@@ -143,49 +145,49 @@ class PayPalReconciler:
 
                statement_post = self.books_posts[0]
 
            except IndexError:
 
                return
 
        for post in self.books_posts:
 
            yield (post, statement_post.compare(post))
 

	
 
    def problems(self) -> int:
 
        stmt_count = len(self.statement_posts)
 
        if stmt_count == 0:
 
            return ReconcileProblems.NOT_IN_STATEMENT
 
        elif stmt_count > 1:
 
            return ReconcileProblems.DUP_IN_STATEMENT
 
        else:
 
            balance = core.Balance(post.amount for post in self.books_posts)
 
            if not (balance - self.statement_posts[0].amount).is_zero():
 
                return ReconcileProblems.AMOUNT_DIFF
 
            worst_problem = 0
 
            for _, problems in self.post_problems():
 
                if problems & ~ReconcileProblems.DATE_DIFF:
 
                    return problems
 
                else:
 
                    worst_problem = worst_problem or problems
 
            return worst_problem
 

	
 
    def sort_key(self) -> Hashable:
 
    def sort_key(self) -> Sortable:
 
        try:
 
            post = self.statement_posts[0]
 
        except IndexError:
 
            post = self.books_posts[0]
 
        return post.date
 

	
 

	
 
class PayPalReconciliationReport(core.BaseODS[PayPalPosting, str]):
 
    COLUMN_WIDTHS = {
 
        'Source': 1.5,
 
        'Transaction ID': 1.5,
 
        'Date': None,
 
        'Amount': 1,
 
        'Description': 2.5,
 
        'Entity': 1.5,
 
    }
 

	
 
    def __init__(self) -> None:
 
        super().__init__()
 
        self.bad_sheet = self.start_sheet("Reconciliation")
 
        self.good_sheet = self.start_sheet("Matched Postings")
 

	
 
    def init_styles(self) -> None:
 
        super().init_styles()
conservancy_beancount/reports/balance_sheet.py
Show inline comments
 
"""balance_sheet.py - Balance sheet report"""
 
# Copyright © 2020  Brett Smith
 
# License: AGPLv3-or-later WITH Beancount-Plugin-Additional-Permission-1.0
 
#
 
# Full copyright and licensing details can be found at toplevel file
 
# LICENSE.txt in the repository.
 

	
 
import argparse
 
import collections
 
import datetime
 
import enum
 
import logging
 
import operator
 
import sys
 

	
 
from decimal import Decimal
 
from pathlib import Path
 

	
 
from typing import (
 
    Any,
 
    Callable,
 
    Collection,
 
    Dict,
 
    Hashable,
 
    Iterable,
 
    Iterator,
 
    List,
 
    Mapping,
 
    NamedTuple,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
    Tuple,
 
    Union,
 
)
 

	
 
import odf.style  # type:ignore[import]
 
import odf.table  # type:ignore[import]
 

	
 
from beancount.parser import printer as bc_printer
 

	
 
from . import core
 
from . import rewrite
 
from .. import books
 
from .. import cliutil
 
from .. import config as configmod
 
from .. import data
 
from .. import ranges
conservancy_beancount/reports/budget.py
Show inline comments
...
 
@@ -6,49 +6,48 @@ the books directly. If there is no ``budget-line`` metadata, it falls back to
 
using account classifications in the account definitions.
 
"""
 
# Copyright © 2020  Brett Smith
 
# License: AGPLv3-or-later WITH Beancount-Plugin-Additional-Permission-1.0
 
#
 
# Full copyright and licensing details can be found at toplevel file
 
# LICENSE.txt in the repository.
 

	
 
import argparse
 
import collections
 
import datetime
 
import enum
 
import logging
 
import operator
 
import sys
 

	
 
from decimal import Decimal
 
from pathlib import Path
 

	
 
from typing import (
 
    Any,
 
    Callable,
 
    Collection,
 
    Dict,
 
    Hashable,
 
    Iterable,
 
    Iterator,
 
    List,
 
    Mapping,
 
    NamedTuple,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
    Tuple,
 
    Union,
 
)
 

	
 
import odf.style  # type:ignore[import]
 
import odf.table  # type:ignore[import]
 

	
 
from beancount.parser import printer as bc_printer
 

	
 
from . import core
 
from . import rewrite
 
from .. import books
 
from .. import cliutil
 
from .. import config as configmod
 
from .. import data
 
from .. import ranges
conservancy_beancount/reports/core.py
Show inline comments
...
 
@@ -31,66 +31,66 @@ import odf.style  # type:ignore[import]
 
import odf.table  # type:ignore[import]
 
import odf.text  # type:ignore[import]
 

	
 
from decimal import Decimal
 
from pathlib import Path
 

	
 
from beancount.core import amount as bc_amount
 
from odf.namespaces import TOOLSVERSION  # type:ignore[import]
 

	
 
from .. import cliutil
 
from .. import data
 
from .. import filters
 
from .. import ranges
 
from .. import rtutil
 

	
 
from typing import (
 
    cast,
 
    overload,
 
    Any,
 
    BinaryIO,
 
    Callable,
 
    Collection,
 
    Dict,
 
    Generic,
 
    Hashable,
 
    Iterable,
 
    Iterator,
 
    List,
 
    Mapping,
 
    MutableMapping,
 
    NamedTuple,
 
    Optional,
 
    Sequence,
 
    Set,
 
    Tuple,
 
    Type,
 
    TypeVar,
 
    Union,
 
)
 
from ..beancount_types import (
 
    MetaKey,
 
    MetaValue,
 
    Sortable,
 
)
 

	
 
OPENING_BALANCE_NAME = "OPENING BALANCE"
 
ENDING_BALANCE_NAME = "ENDING BALANCE"
 

	
 
DecimalCompat = data.DecimalCompat
 
BalanceType = TypeVar('BalanceType', bound='Balance')
 
ElementType = Callable[..., odf.element.Element]
 
LinkType = Union[str, Tuple[str, Optional[str]]]
 
RelatedType = TypeVar('RelatedType', bound='RelatedPostings')
 
RT = TypeVar('RT', bound=Sequence)
 
ST = TypeVar('ST')
 
T = TypeVar('T')
 

	
 
class Balance(Mapping[str, data.Amount]):
 
    """A collection of amounts mapped by currency
 

	
 
    Each key is a Beancount currency string, and each value represents the
 
    balance in that currency.
 
    """
 
    __slots__ = ('_currency_map', 'tolerance')
 
    TOLERANCE = Decimal('0.01')
 

	
 
    def __init__(self,
...
 
@@ -418,49 +418,49 @@ class Balances:
 
    ) -> Sequence[data.Account]:
 
        """Return a sequence of seen account classifications
 

	
 
        Given an account name, returns a sequence of all the account
 
        classifications seen in the postings under that part of the account
 
        hierarchy. The classifications are sorted in descending order by the
 
        balance of postings under them for the ``sort_period`` time period.
 
        """
 
        if sort_period is None:
 
            if account in data.EQUITY_ACCOUNTS:
 
                sort_period = Period.PERIOD
 
            else:
 
                sort_period = Period.ANY
 
        class_bals: Mapping[data.Account, MutableBalance] \
 
            = collections.defaultdict(MutableBalance)
 
        for key, balance in self.balances.items():
 
            if not key.account.is_under(account):
 
                pass
 
            elif key.period & sort_period:
 
                class_bals[key.classification] += balance
 
            else:
 
                # Ensure the balance exists in the mapping
 
                class_bals[key.classification]
 
        norm_func = normalize_amount_func(f'{account}:RootsOK')
 
        def sortkey(acct: data.Account) -> Hashable:
 
        def sortkey(acct: data.Account) -> Sortable:
 
            prefix, _, _ = acct.rpartition(':')
 
            balance = norm_func(class_bals[acct])
 
            try:
 
                max_bal = max(amount.number for amount in balance.values())
 
            except ValueError:
 
                max_bal = Decimal(0)
 
            return prefix, -max_bal
 
        return sorted(class_bals, key=sortkey)
 

	
 
    def iter_accounts(self, root: Optional[str]=None) -> Sequence[data.Account]:
 
        """Return a sequence of accounts open during the reporting period
 

	
 
        The sequence is sorted by account name.
 
        """
 
        start_date = self.period_range.start
 
        stop_date = self.period_range.stop
 
        return sorted(
 
            account
 
            for account in data.Account.iter_accounts(root)
 
            if account.meta.open_date < stop_date
 
            and (account.meta.close_date is None
 
                 or account.meta.close_date > start_date)
 
        )
 

	
conservancy_beancount/reports/ledger.py
Show inline comments
...
 
@@ -25,63 +25,63 @@ Report all activity related to a given project::
 

	
 
Get all Assets postings for a given month to help with reconciliation::
 

	
 
    ledger-report -a Assets -b 2018-05-01 -e 2018-06-01
 
"""
 
# Copyright © 2020  Brett Smith
 
# License: AGPLv3-or-later WITH Beancount-Plugin-Additional-Permission-1.0
 
#
 
# Full copyright and licensing details can be found at toplevel file
 
# LICENSE.txt in the repository.
 

	
 
import argparse
 
import collections
 
import datetime
 
import enum
 
import itertools
 
import operator
 
import logging
 
import sys
 

	
 
from typing import (
 
    Any,
 
    Callable,
 
    Dict,
 
    Hashable,
 
    Iterable,
 
    Iterator,
 
    List,
 
    Mapping,
 
    Optional,
 
    Sequence,
 
    Set,
 
    TextIO,
 
    Tuple,
 
    Type,
 
    Union,
 
    cast,
 
)
 
from ..beancount_types import (
 
    Sortable,
 
    Transaction,
 
)
 

	
 
from pathlib import Path
 

	
 
import odf.table  # type:ignore[import]
 
import odf.text  # type:ignore[import]
 

	
 
from beancount.core import data as bc_data
 
from beancount.parser import printer as bc_printer
 

	
 
from . import core
 
from . import rewrite
 
from .. import books
 
from .. import cliutil
 
from .. import config as configmod
 
from .. import data
 
from .. import ranges
 
from .. import rtutil
 

	
 
PostTally = List[Tuple[int, data.Account]]
 

	
 
PROGNAME = 'ledger-report'
 
logger = logging.getLogger('conservancy_beancount.reports.ledger')
...
 
@@ -509,49 +509,49 @@ class FundLedgerODS(LedgerODS):
 
class ReportType(enum.IntFlag):
 
    ZERO_TRANSACTIONS = enum.auto()
 
    ZERO_TXNS = ZERO_TRANSACTIONS
 
    CREDIT_TRANSACTIONS = enum.auto()
 
    CREDIT_TXNS = CREDIT_TRANSACTIONS
 
    DEBIT_TRANSACTIONS = enum.auto()
 
    DEBIT_TXNS = DEBIT_TRANSACTIONS
 
    ALL_TRANSACTIONS = ZERO_TRANSACTIONS | CREDIT_TRANSACTIONS | DEBIT_TRANSACTIONS
 
    ALL_TXNS = ALL_TRANSACTIONS
 
    FULL_LEDGER = enum.auto()
 
    FUND_LEDGER = enum.auto()
 
    PROJECT_LEDGER = FUND_LEDGER
 

	
 
    @classmethod
 
    def post_flag(cls, post: data.Posting) -> int:
 
        norm_func = core.normalize_amount_func(post.account)
 
        number = norm_func(post.units.number)
 
        if not number:
 
            return cls.ZERO_TRANSACTIONS
 
        elif number > 0:
 
            return cls.CREDIT_TRANSACTIONS
 
        else:
 
            return cls.DEBIT_TRANSACTIONS
 

	
 
    def _choices_sortkey(self) -> Hashable:
 
    def _choices_sortkey(self) -> Sortable:
 
        subtype, _, maintype = self.name.partition('_')
 
        return (maintype, subtype)
 

	
 

	
 
class TransactionODS(LedgerODS):
 
    CORE_COLUMNS: Sequence[str] = [
 
        'Date',
 
        'Description',
 
        'Account',
 
        data.Metadata.human_name('entity'),
 
        'Original Amount',
 
        'Booked Amount',
 
    ]
 
    METADATA_COLUMNS: Sequence[str] = [
 
        'project',
 
        'rt-id',
 
        'receipt',
 
        'check',
 
        'invoice',
 
        'contract',
 
        'approval',
 
        'paypal-id',
 
        'check-number',
 
        'bank-statement',
conservancy_beancount/tools/opening_balances.py
Show inline comments
...
 
@@ -6,98 +6,98 @@ it to stdout. Use this when you close the books for a year to record the final
 
balances for that year.
 

	
 
Run it without arguments to generate opening balances for the current fiscal
 
year. You can also specify a fiscal year to generate opening balances for, or
 
even a specific date (which can be helpful for testing or debugging).
 
"""
 
# SPDX-FileCopyrightText: © 2020 Martin Michlmayr <tbm@cyrius.com>
 
# SPDX-FileCopyrightText: © 2020 Brett Smith
 
# License: AGPLv3-or-later WITH Beancount-Plugin-Additional-Permission-1.0
 
#
 
# Full copyright and licensing details can be found at toplevel file
 
# LICENSE.txt in the repository.
 

	
 
import argparse
 
import collections
 
import copy
 
import datetime
 
import enum
 
import locale
 
import logging
 
import sys
 

	
 
from typing import (
 
    Dict,
 
    Hashable,
 
    Iterable,
 
    Iterator,
 
    Mapping,
 
    NamedTuple,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
    Tuple,
 
)
 
from ..beancount_types import (
 
    Error,
 
    MetaKey,
 
    MetaValue,
 
    Sortable,
 
    Transaction,
 
)
 

	
 
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP
 

	
 
from .. import books
 
from .. import cliutil
 
from .. import config as configmod
 
from .. import data
 
from ..reports.core import Balance
 

	
 
from beancount.core import data as bc_data
 
from beancount.core import display_context as bc_dcontext
 
from beancount.parser import printer as bc_printer
 

	
 
from beancount.core.convert import get_cost
 
from beancount.core.inventory import Inventory
 
from beancount.core.position import Position, get_position
 

	
 
RESTRICTED_ACCOUNT = data.Account('Equity:Funds:Restricted')
 
UNRESTRICTED_ACCOUNT = data.Account('Equity:Funds:Unrestricted')
 
PROGNAME = 'opening-balances'
 
logger = logging.getLogger('conservancy_beancount.tools.opening_balances')
 

	
 
def quantize_amount(
 
        amount: data.Amount,
 
        exp: Decimal=Decimal('.01'),
 
        rounding: str=ROUND_HALF_EVEN,
 
) -> data.Amount:
 
    return amount._replace(number=amount.number.quantize(exp, rounding=rounding))
 

	
 
class AccountWithFund(NamedTuple):
 
    account: data.Account
 
    fund: Optional[MetaValue]
 

	
 
    def sortkey(self) -> Hashable:
 
    def sortkey(self) -> Sortable:
 
        account, fund = self
 
        return (
 
            0 if fund is None else 1,
 
            locale.strxfrm(account),
 
            locale.strxfrm(str(fund).casefold()),
 
        )
 

	
 

	
 
class Posting(data.Posting):
 
    @staticmethod
 
    def _position_sortkey(position: Position) -> str:
 
        units, cost = position
 
        if cost is None:
 
            # Beancount type-declares that position.cost must be a Cost, but
 
            # in practice that's not true. Call get_position(post) on any
 
            # post without a cost and see what it returns. Hence the ignore.
 
            return units.currency  # type:ignore[unreachable]
 
        else:
 
            return f'{units.currency} {cost.currency} {cost.date.isoformat()}'
 

	
 
    @classmethod
 
    def build_opening(
 
            cls,
 
            key: AccountWithFund,
conservancy_beancount/tools/sort_entries.py
Show inline comments
 
#!/usr/bin/env python3
 
"""sort_entries.py - Consistently sort and format Beancount entries
 

	
 
This is useful to use as a preprocessing step before comparing entries with
 
tools like ``diff``.
 
"""
 
# Copyright © 2020 Brett Smith
 
# License: AGPLv3-or-later WITH Beancount-Plugin-Additional-Permission-1.0
 
#
 
# Full copyright and licensing details can be found at toplevel file
 
# LICENSE.txt in the repository.
 

	
 
import argparse
 
import logging
 
import os
 
import sys
 

	
 
from pathlib import Path
 

	
 
from beancount.core import display_context as bc_dcontext
 
from beancount import loader as bc_loader
 
from beancount.parser import printer as bc_printer
 

	
 
from typing import (
 
    Hashable,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
)
 
from ..beancount_types import (
 
    Directive,
 
    Entries,
 
    Errors,
 
    Sortable,
 
)
 

	
 
from .. import cliutil
 

	
 
PROGNAME = 'bean-sort'
 
logger = logging.getLogger('conservancy_beancount.tools.sort_entries')
 

	
 
def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace:
 
    parser = argparse.ArgumentParser(prog=PROGNAME)
 
    cliutil.add_version_argument(parser)
 
    cliutil.add_loglevel_argument(parser)
 
    parser.add_argument(
 
        '--quiet', '-q',
 
        action='store_true',
 
        help="""Suppress Beancount errors
 
""")
 
    parser.add_argument(
 
        'paths',
 
        metavar='PATH',
 
        type=Path,
 
        nargs=argparse.ONE_OR_MORE,
 
        help="""Beancount path(s) to read entries from
 
""")
 
    return parser.parse_args(arglist)
 

	
 
def entry_sorter(entry: Directive) -> Hashable:
 
def entry_sorter(entry: Directive) -> Sortable:
 
    type_name = type(entry).__name__
 
    if type_name == 'Transaction':
 
        return (entry.date, type_name, entry.narration, entry.payee or '')  # type:ignore[attr-defined]
 
    else:
 
        return (entry.date, type_name)
 

	
 
def main(arglist: Optional[Sequence[str]]=None,
 
         stdout: TextIO=sys.stdout,
 
         stderr: TextIO=sys.stderr,
 
) -> int:
 
    args = parse_arguments(arglist)
 
    cliutil.set_loglevel(logger, args.loglevel)
 

	
 
    entries: Entries = []
 
    errors: Errors = []
 
    for path in args.paths:
 
        new_entries, new_errors, _ = bc_loader.load_file(path)
 
        entries.extend(new_entries)
 
        errors.extend(new_errors)
 
    entries.sort(key=entry_sorter)
 
    if not args.quiet:
 
        for error in errors:
 
            bc_printer.print_error(error, file=stderr)
 
    dcontext = bc_dcontext.DisplayContext()
0 comments (0 inline, 0 general)