plugin.core: Document base classes.
Since so much other code is built on these, I wanted to make sure I wrote this while it was all fresh in my head.
This commit is contained in:
parent
6e0c31f0ab
commit
c3c19b0ad2
1 changed files with 60 additions and 0 deletions
|
@ -19,10 +19,22 @@ import re
|
||||||
|
|
||||||
from . import errors as errormod
|
from . import errors as errormod
|
||||||
|
|
||||||
|
# I expect these will become configurable in the future, which is why I'm
|
||||||
|
# keeping them outside of a class, but for now constants will do.
|
||||||
DEFAULT_START_DATE = datetime.date(2020, 3, 1)
|
DEFAULT_START_DATE = datetime.date(2020, 3, 1)
|
||||||
|
# The default stop date leaves a little room after so it's easy to test
|
||||||
|
# dates past the far end of the range.
|
||||||
DEFAULT_STOP_DATE = datetime.date(datetime.MAXYEAR, 1, 1)
|
DEFAULT_STOP_DATE = datetime.date(datetime.MAXYEAR, 1, 1)
|
||||||
|
|
||||||
class _GenericRange:
|
class _GenericRange:
|
||||||
|
"""Convenience class to check whether a value is within a range.
|
||||||
|
|
||||||
|
`foo in generic_range` is equivalent to `start <= foo < stop`.
|
||||||
|
Since we have multiple user-configurable ranges, having the check
|
||||||
|
encapsulated in an object helps implement the check consistently, and
|
||||||
|
makes it easier for subclasses to override.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, start, stop):
|
def __init__(self, start, stop):
|
||||||
self.start = start
|
self.start = start
|
||||||
self.stop = stop
|
self.stop = stop
|
||||||
|
@ -38,7 +50,26 @@ class _GenericRange:
|
||||||
|
|
||||||
|
|
||||||
class MetadataEnum:
|
class MetadataEnum:
|
||||||
|
"""Map acceptable metadata values to their normalized forms.
|
||||||
|
|
||||||
|
When a piece of metadata uses a set of allowed values, use this class to
|
||||||
|
define them. You can also specify aliases that hooks will normalize to
|
||||||
|
the primary values.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, key, standard_values, aliases_map):
|
def __init__(self, key, standard_values, aliases_map):
|
||||||
|
"""Specify allowed values and aliases for this metadata.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
* key: The name of the metadata key that uses this enum.
|
||||||
|
* standard_values: A sequence of strings that enumerate the standard
|
||||||
|
values for this metadata.
|
||||||
|
* aliases_map: A mapping of strings to strings. The keys are
|
||||||
|
additional allowed metadata values. The values are standard values
|
||||||
|
that each key will evaluate to. The code asserts that all values are
|
||||||
|
in standard_values.
|
||||||
|
"""
|
||||||
self.key = key
|
self.key = key
|
||||||
self._stdvalues = frozenset(standard_values)
|
self._stdvalues = frozenset(standard_values)
|
||||||
self._aliases = dict(aliases_map)
|
self._aliases = dict(aliases_map)
|
||||||
|
@ -49,15 +80,26 @@ class MetadataEnum:
|
||||||
return "{}<{}>".format(type(self).__name__, self.key)
|
return "{}<{}>".format(type(self).__name__, self.key)
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
|
"""Returns true if `key` is a standard value or alias."""
|
||||||
return key in self._aliases
|
return key in self._aliases
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
|
"""Return the standard value for `key`.
|
||||||
|
|
||||||
|
Raises KeyError if `key` is not a known value or alias.
|
||||||
|
"""
|
||||||
return self._aliases[key]
|
return self._aliases[key]
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
|
"""Iterate over standard values."""
|
||||||
return iter(self._stdvalues)
|
return iter(self._stdvalues)
|
||||||
|
|
||||||
def get(self, key, default_key=None):
|
def get(self, key, default_key=None):
|
||||||
|
"""Return self[key], or a default fallback if that doesn't exist.
|
||||||
|
|
||||||
|
default_key is another key to look up, *not* a default value to return.
|
||||||
|
This helps ensure you always get a standard value.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return self[key]
|
return self[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -68,6 +110,17 @@ class MetadataEnum:
|
||||||
|
|
||||||
|
|
||||||
class PostingChecker:
|
class PostingChecker:
|
||||||
|
"""Base class to normalize posting metadata from an enum."""
|
||||||
|
# This class provides basic functionality to filter postings, normalize
|
||||||
|
# metadata values, and set default values.
|
||||||
|
# Subclasses should set:
|
||||||
|
# * METADATA_KEY: A string with the name of the metadata key to normalize.
|
||||||
|
# * ACCOUNTS: Only check postings that match these account names.
|
||||||
|
# Can be a tuple of account prefix strings, or a regexp.
|
||||||
|
# * VALUES_ENUM: A MetadataEnum with allowed values and aliases.
|
||||||
|
# Subclasses may wish to override _default_value and _should_check.
|
||||||
|
# See below.
|
||||||
|
|
||||||
HOOK_GROUPS = frozenset(['Posting', 'metadata'])
|
HOOK_GROUPS = frozenset(['Posting', 'metadata'])
|
||||||
ACCOUNTS = ('',)
|
ACCOUNTS = ('',)
|
||||||
TXN_DATE_RANGE = _GenericRange(DEFAULT_START_DATE, DEFAULT_STOP_DATE)
|
TXN_DATE_RANGE = _GenericRange(DEFAULT_START_DATE, DEFAULT_STOP_DATE)
|
||||||
|
@ -84,9 +137,16 @@ class PostingChecker:
|
||||||
post.meta = {}
|
post.meta = {}
|
||||||
post.meta[key] = value
|
post.meta[key] = value
|
||||||
|
|
||||||
|
# If the posting does not specify METADATA_KEY, the hook calls
|
||||||
|
# _default_value to get a default. This method should either return
|
||||||
|
# a value string from METADATA_ENUM, or else raise InvalidMetadataError.
|
||||||
|
# This base implementation does the latter.
|
||||||
def _default_value(self, txn, post):
|
def _default_value(self, txn, post):
|
||||||
raise errormod.InvalidMetadataError(txn, post, self.METADATA_KEY)
|
raise errormod.InvalidMetadataError(txn, post, self.METADATA_KEY)
|
||||||
|
|
||||||
|
# The hook calls _should_check on every posting and only checks postings
|
||||||
|
# when the method returns true. This base method checks the transaction
|
||||||
|
# date is in TXN_DATE_RANGE, and the posting account name matches ACCOUNTS.
|
||||||
def _should_check(self, txn, post):
|
def _should_check(self, txn, post):
|
||||||
ok = txn.date in self.TXN_DATE_RANGE
|
ok = txn.date in self.TXN_DATE_RANGE
|
||||||
if isinstance(self.ACCOUNTS, tuple):
|
if isinstance(self.ACCOUNTS, tuple):
|
||||||
|
|
Loading…
Reference in a new issue