"""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 (
MetaKey,
MetaValue,
Transaction,
)
from typing import (
MutableMapping,
Optional,
Pattern,
Tuple,
)
class MetaEntity(core.TransactionHook):
METADATA_KEY = 'entity'
HOOK_GROUPS = frozenset(['posting', 'metadata', METADATA_KEY])
# chars is the set of characters we always accept in entity metadata:
# letters, digits, and ASCII punctuation, except `-` and the Latin 1 supplement
# (i.e., Roman letters with diacritics: áÁàÀâÂåÅäÄãà çÇ ðÐ ñÑ øØ ß etc.)
# See the tests for specific cases.
chars = r'\u0021-\u002c\u002e-\u007e\p{Letter}\p{Digit}--\p{Block=Latin_1_Supplement}'
ENTITY_RE: Pattern[str] = regex.compile(f'^[{chars}][-{chars}]*$', regex.VERSION1)
ANONYMOUS_RE: Pattern[str] = regex.compile(r'^[-_.?!\s]*$', regex.VERSION1)
del chars
def _check_entity(self,
meta: MutableMapping[MetaKey, MetaValue],
default: Optional[str]=None,
) -> Tuple[Optional[str], Optional[bool]]:
entity = meta.get(self.METADATA_KEY, default)
if entity is None:
return None, None
elif not isinstance(entity, str):
return None, False
elif self.ANONYMOUS_RE.match(entity):
entity = 'Anonymous'
meta[self.METADATA_KEY] = entity
return entity, True
else:
return entity, self.ENTITY_RE.match(entity) is not None
def run(self, txn: Transaction) -> errormod.Iter:
if not self._run_on_txn(txn):
return
txn_entity, txn_entity_ok = self._check_entity(txn.meta, txn.payee)
if txn_entity_ok is False:
yield errormod.InvalidMetadataError(txn, self.METADATA_KEY, txn_entity)
if txn_entity is txn.payee:
txn.meta[self.METADATA_KEY] = txn.payee
for post in data.Posting.from_txn(txn):
if not post.account.is_under(
'Assets:Receivable',
'Expenses',
'Income',
'Liabilities:Payable',
):
continue
entity, entity_ok = self._check_entity(post.meta, txn_entity)
if entity is txn_entity and entity is not None:
pass
elif not entity_ok:
yield errormod.InvalidMetadataError(txn, self.METADATA_KEY, entity, post)