75 lines
3.1 KiB
Python
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)
|