conservancy_beancount/conservancy_beancount/plugin/meta_entity.py

75 lines
3.1 KiB
Python

"""meta_entity - Validate entity metadata"""
# 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/>.
# Type stubs aren't available for regex.
# Fortunately, we're using it in a way that's API-compatible with the re
# module. We mitigate the lack of type stubs by providing type declarations
# for returned objects. This way, the only thing that isn't type checked are
# the calls to regex functions.
import regex # type:ignore[import]
from . import core
from .. import data
from .. import errors as errormod
from ..beancount_types import (
Transaction,
)
from typing import (
Pattern,
)
class MetaEntity(core.TransactionHook):
METADATA_KEY = 'entity'
HOOK_GROUPS = frozenset(['posting', 'metadata', METADATA_KEY])
# alnum is the set of characters we always accept in entity metadata:
# letters and digits, minus the Latin 1 supplement (i.e., Roman letters
# with diacritics: áÁàÀâÂåÅäÄãà çÇ ðÐ ñÑ øØ ß etc.)
# See the tests for specific cases.
alnum = r'\p{Letter}\p{Digit}--\p{Block=Latin_1_Supplement}'
# A regexp that would be reasonably stricter would be:
# f'^[{alnum}][.{alnum}]*(?:-[.{alnum}])*$'
# However, current producers fail that regexp in a few different ways.
# See the tests for specific cases.
ENTITY_RE: Pattern[str] = regex.compile(f'^[{alnum}][-.{alnum}]*$', regex.VERSION1)
del alnum
def run(self, txn: Transaction) -> errormod.Iter:
txn_entity = txn.meta.get(self.METADATA_KEY, txn.payee)
if txn_entity is None:
txn_entity_ok = None
elif isinstance(txn_entity, str):
txn_entity_ok = bool(self.ENTITY_RE.match(txn_entity))
else:
txn_entity_ok = False
if txn_entity_ok is False:
yield errormod.InvalidMetadataError(txn, self.METADATA_KEY, txn_entity)
for post in data.iter_postings(txn):
if not post.account.is_under(
'Assets:Receivable',
'Expenses',
'Income',
'Liabilities:Payable',
):
continue
entity = post.meta.get(self.METADATA_KEY)
if entity is None:
yield errormod.InvalidMetadataError(txn, self.METADATA_KEY, entity, post)
elif entity is txn_entity:
pass
elif not self.ENTITY_RE.match(entity):
yield errormod.InvalidMetadataError(txn, self.METADATA_KEY, entity, post)