
See the test cases for examples of real entities in the books that we should accept for now.
87 lines
3.4 KiB
Python
87 lines
3.4 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 (
|
|
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 data.is_opening_balance_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)
|
|
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)
|