diff --git a/CHANGELOG b/CHANGELOG index 038b1d8..ab52a6f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +- 0.1.1 + * Add optional, pluggable verifier to protect state transition +- 0.1.0 + * Release version bump - 0.0.19: * Enable alias with comma separated values - 0.0.18 diff --git a/setup.cfg b/setup.cfg index 43b0318..663c5ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = shep -version = 0.1.0rc1 +version = 0.1.1rc1 description = Multi-state key stores using bit masks author = Louis Holbrook author_email = dev@holbrook.no diff --git a/shep/error.py b/shep/error.py index bd4beb9..74223bf 100644 --- a/shep/error.py +++ b/shep/error.py @@ -26,3 +26,9 @@ class StateCorruptionError(RuntimeError): """An irrecoverable discrepancy between persisted state and memory state has occurred. """ pass + + +class StateTransitionInvalid(Exception): + """Raised if state transition verification fails + """ + pass diff --git a/shep/state.py b/shep/state.py index 23d599e..1e00ede 100644 --- a/shep/state.py +++ b/shep/state.py @@ -1,12 +1,18 @@ +# standard imports +import re + # local imports from shep.error import ( StateExists, StateInvalid, StateItemExists, StateItemNotFound, + StateTransitionInvalid, ) +re_name = r'^[a-zA-Z_]+$' + class State: """State is an in-memory bitmasked state store for key-value pairs, or even just keys alone. @@ -22,17 +28,17 @@ class State: base_state_name = 'NEW' - def __init__(self, bits, logger=None): + def __init__(self, bits, logger=None, verifier=None): self.__bits = bits self.__limit = (1 << bits) - 1 self.__c = 0 setattr(self, self.base_state_name, 0) - #self.NEW = 0 self.__reverse = {0: getattr(self, self.base_state_name)} self.__keys = {getattr(self, self.base_state_name): []} self.__keys_reverse = {} self.__contents = {} + self.verifier = verifier @classmethod @@ -54,8 +60,8 @@ class State: # validates a state name and return its canonical representation def __check_name_valid(self, k): - if not k.isalpha(): - raise ValueError('only alpha') + if not re.match(re_name, k): + raise ValueError('only alpha and underscore') return k.upper() @@ -323,6 +329,11 @@ class State: if current_state_list == None: raise StateCorruptionError(to_state) + if self.verifier != None: + r = self.verifier(self, from_state, to_state) + if r != None: + raise StateTransitionInvalid('{} -> {}: {}'.format(from_state, to_state, r)) + self.__add_state_list(to_state, key) current_state_list.pop(idx) diff --git a/shep/verify.py b/shep/verify.py new file mode 100644 index 0000000..a238932 --- /dev/null +++ b/shep/verify.py @@ -0,0 +1,2 @@ +def default_checker(statestore, old, new): + return None diff --git a/tests/test_state.py b/tests/test_state.py index da7a06c..b5de3c6 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -18,7 +18,6 @@ class TestState(unittest.TestCase): for k in [ 'f0o', 'f oo', - 'f_oo', ]: with self.assertRaises(ValueError): states.add(k)