"""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 . # 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: if data.is_opening_balance_txn(txn): return 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.Posting.from_txn(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)