2022-03-11 11:31:08 +01:00
|
|
|
|
# standard imports
|
|
|
|
|
import re
|
2022-03-13 17:36:17 +01:00
|
|
|
|
import datetime
|
2022-03-11 11:31:08 +01:00
|
|
|
|
|
2022-01-31 09:32:48 +01:00
|
|
|
|
# local imports
|
2022-01-31 10:33:21 +01:00
|
|
|
|
from shep.error import (
|
2022-01-31 09:38:14 +01:00
|
|
|
|
StateExists,
|
|
|
|
|
StateInvalid,
|
2022-01-31 12:23:51 +01:00
|
|
|
|
StateItemExists,
|
|
|
|
|
StateItemNotFound,
|
2022-03-11 11:31:08 +01:00
|
|
|
|
StateTransitionInvalid,
|
2022-03-11 20:36:24 +01:00
|
|
|
|
StateCorruptionError,
|
2022-01-31 09:38:14 +01:00
|
|
|
|
)
|
2022-01-31 09:32:48 +01:00
|
|
|
|
|
|
|
|
|
|
2022-03-31 18:00:27 +02:00
|
|
|
|
re_name = r'^[a-zA-Z_\.]+$'
|
2022-03-11 11:31:08 +01:00
|
|
|
|
|
2022-01-31 09:32:48 +01:00
|
|
|
|
class State:
|
2022-02-06 21:43:20 +01:00
|
|
|
|
"""State is an in-memory bitmasked state store for key-value pairs, or even just keys alone.
|
2022-01-31 09:32:48 +01:00
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
A State is comprised of a number of atomic state bits, and zero or more aliases that represent unique combinations of these bits.
|
|
|
|
|
|
|
|
|
|
The State object will enforce that duplicate states cannot exist. It will also enforce that all alias states are composed of valid atomic states.
|
|
|
|
|
|
|
|
|
|
:param bits: Number of atomic states that this State object will represent (i.e. number of bits).
|
|
|
|
|
:type bits: int
|
|
|
|
|
:param logger: Standard library logging instance to output to
|
|
|
|
|
:type logger: logging.Logger
|
|
|
|
|
"""
|
2022-02-09 20:20:59 +01:00
|
|
|
|
|
|
|
|
|
base_state_name = 'NEW'
|
|
|
|
|
|
2022-03-16 18:13:05 +01:00
|
|
|
|
def __init__(self, bits, logger=None, verifier=None, check_alias=True, event_callback=None):
|
2022-03-16 20:25:29 +01:00
|
|
|
|
self.__initial_bits = bits
|
2022-01-31 09:32:48 +01:00
|
|
|
|
self.__bits = bits
|
2022-01-31 09:38:14 +01:00
|
|
|
|
self.__limit = (1 << bits) - 1
|
2022-01-31 09:32:48 +01:00
|
|
|
|
self.__c = 0
|
2022-02-09 20:20:59 +01:00
|
|
|
|
setattr(self, self.base_state_name, 0)
|
2022-01-31 13:10:04 +01:00
|
|
|
|
|
2022-02-09 20:20:59 +01:00
|
|
|
|
self.__reverse = {0: getattr(self, self.base_state_name)}
|
|
|
|
|
self.__keys = {getattr(self, self.base_state_name): []}
|
2022-02-01 07:29:51 +01:00
|
|
|
|
self.__keys_reverse = {}
|
|
|
|
|
self.__contents = {}
|
2022-03-16 17:49:00 +01:00
|
|
|
|
self.modified_last = {}
|
2022-03-11 11:31:08 +01:00
|
|
|
|
self.verifier = verifier
|
2022-03-16 17:49:00 +01:00
|
|
|
|
self.check_alias = check_alias
|
2022-03-16 18:13:05 +01:00
|
|
|
|
self.event_callback = event_callback
|
2022-01-31 09:32:48 +01:00
|
|
|
|
|
|
|
|
|
|
2022-02-09 20:20:59 +01:00
|
|
|
|
@classmethod
|
|
|
|
|
def set_default_state(cls, state_name):
|
|
|
|
|
cls.base_state_name = state_name.upper()
|
|
|
|
|
|
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
# return true if v is a single-bit state
|
2022-05-06 09:18:59 +02:00
|
|
|
|
def is_pure(self, v):
|
2022-02-02 08:59:52 +01:00
|
|
|
|
if v == 0:
|
|
|
|
|
return True
|
2022-01-31 09:38:14 +01:00
|
|
|
|
c = 1
|
|
|
|
|
for i in range(self.__bits):
|
|
|
|
|
if c & v > 0:
|
|
|
|
|
break
|
|
|
|
|
c <<= 1
|
|
|
|
|
return c == v
|
2022-01-31 09:32:48 +01:00
|
|
|
|
|
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
# validates a state name and return its canonical representation
|
2022-01-31 12:23:51 +01:00
|
|
|
|
def __check_name_valid(self, k):
|
2022-03-11 11:31:08 +01:00
|
|
|
|
if not re.match(re_name, k):
|
|
|
|
|
raise ValueError('only alpha and underscore')
|
2022-02-03 11:05:59 +01:00
|
|
|
|
return k.upper()
|
2022-01-31 12:23:51 +01:00
|
|
|
|
|
2022-02-02 08:59:52 +01:00
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
# enforces name validity, aswell as name uniqueness
|
2022-01-31 12:23:51 +01:00
|
|
|
|
def __check_name(self, k):
|
2022-02-03 11:05:59 +01:00
|
|
|
|
k = self.__check_name_valid(k)
|
|
|
|
|
|
2022-01-31 09:32:48 +01:00
|
|
|
|
try:
|
|
|
|
|
getattr(self, k)
|
|
|
|
|
raise StateExists(k)
|
|
|
|
|
except AttributeError:
|
|
|
|
|
pass
|
2022-01-31 09:38:14 +01:00
|
|
|
|
return k
|
2022-01-31 09:32:48 +01:00
|
|
|
|
|
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
# enforces state value validity and uniqueness
|
2022-01-31 10:33:21 +01:00
|
|
|
|
def __check_valid(self, v):
|
2022-02-06 09:11:47 +01:00
|
|
|
|
v = self.__check_value_typ(v)
|
2022-01-31 09:38:14 +01:00
|
|
|
|
if self.__reverse.get(v):
|
2022-01-31 10:55:56 +01:00
|
|
|
|
raise StateExists(v)
|
2022-01-31 10:33:21 +01:00
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
# enforces state value within bit limit of instantiation
|
2022-03-18 19:34:49 +01:00
|
|
|
|
def __check_limit(self, v, pure=True):
|
|
|
|
|
if pure:
|
|
|
|
|
if self.__initial_bits == 0:
|
|
|
|
|
self.__bits += 1
|
|
|
|
|
self.__limit = (1 << self.__bits) - 1
|
2022-01-31 09:38:14 +01:00
|
|
|
|
if v > self.__limit:
|
|
|
|
|
raise OverflowError(v)
|
2022-02-06 09:11:47 +01:00
|
|
|
|
return v
|
2022-02-02 08:59:52 +01:00
|
|
|
|
|
2022-02-04 23:15:51 +01:00
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
# enforces state value validity, uniqueness and value limit
|
2022-02-02 08:59:52 +01:00
|
|
|
|
def __check_value(self, v):
|
|
|
|
|
v = self.__check_valid(v)
|
|
|
|
|
self.__check_limit(v)
|
2022-01-31 09:38:14 +01:00
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
# enforces state value validity
|
2022-02-06 09:11:47 +01:00
|
|
|
|
def __check_value_typ(self, v):
|
|
|
|
|
return int(v)
|
|
|
|
|
|
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
# enforces state value validity within the currently registered states (number of add calls vs number of bits in instantiation).
|
2022-01-31 10:33:21 +01:00
|
|
|
|
def __check_value_cursor(self, v):
|
2022-02-06 09:11:47 +01:00
|
|
|
|
v = self.__check_value_typ(v)
|
2022-01-31 10:33:21 +01:00
|
|
|
|
if v > 1 << self.__c:
|
|
|
|
|
raise StateInvalid(v)
|
|
|
|
|
return v
|
2022-01-31 09:38:14 +01:00
|
|
|
|
|
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
# set a bit for state of the given key
|
2022-01-31 09:38:14 +01:00
|
|
|
|
def __set(self, k, v):
|
|
|
|
|
setattr(self, k, v)
|
|
|
|
|
self.__reverse[v] = k
|
2022-01-31 09:32:48 +01:00
|
|
|
|
self.__c += 1
|
2022-01-31 09:38:14 +01:00
|
|
|
|
|
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
# check validity of key to register state for
|
2022-02-01 07:29:51 +01:00
|
|
|
|
def __check_key(self, item):
|
|
|
|
|
if self.__keys_reverse.get(item) != None:
|
2022-01-31 12:23:51 +01:00
|
|
|
|
raise StateItemExists(item)
|
|
|
|
|
|
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
# adds a new key to the state store
|
2022-01-31 12:23:51 +01:00
|
|
|
|
def __add_state_list(self, state, item):
|
2022-02-01 07:29:51 +01:00
|
|
|
|
if self.__keys.get(state) == None:
|
|
|
|
|
self.__keys[state] = []
|
2022-05-06 09:18:59 +02:00
|
|
|
|
if not self.is_pure(state) or state == 0:
|
2022-03-11 20:36:24 +01:00
|
|
|
|
self.__keys[state].append(item)
|
|
|
|
|
c = 1
|
|
|
|
|
for i in range(self.__bits):
|
|
|
|
|
part = c & state
|
|
|
|
|
if part > 0:
|
|
|
|
|
if self.__keys.get(part) == None:
|
|
|
|
|
self.__keys[part] = []
|
|
|
|
|
self.__keys[part].append(item)
|
|
|
|
|
c <<= 1
|
2022-02-01 07:29:51 +01:00
|
|
|
|
self.__keys_reverse[item] = state
|
2022-03-29 11:24:57 +02:00
|
|
|
|
if self.__reverse.get(state) == None and not self.check_alias:
|
2022-03-24 00:34:13 +01:00
|
|
|
|
s = self.elements(state)
|
2022-03-31 10:34:51 +02:00
|
|
|
|
self.__alias(s, state)
|
2022-01-31 12:23:51 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __state_list_index(self, item, state_list):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Get index of a key for a given state.
|
|
|
|
|
A key should only ever exist in one state.
|
|
|
|
|
A failed lookup should indicate a mistake on the caller part, (it may also indicate corruption, but probanbly impossible to tell the difference)
|
|
|
|
|
"""
|
2022-01-31 12:23:51 +01:00
|
|
|
|
idx = -1
|
|
|
|
|
try:
|
|
|
|
|
idx = state_list.index(item)
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
if idx == -1:
|
|
|
|
|
raise StateCorruptionError() # should have state int here as value
|
|
|
|
|
|
|
|
|
|
return idx
|
|
|
|
|
|
|
|
|
|
|
2022-01-31 09:38:14 +01:00
|
|
|
|
def add(self, k):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Add a state to the store.
|
|
|
|
|
|
|
|
|
|
:param k: State name
|
|
|
|
|
:type k: str
|
|
|
|
|
:raises shep.error.StateExists: State name is already registered
|
|
|
|
|
"""
|
2022-01-31 09:38:14 +01:00
|
|
|
|
v = 1 << self.__c
|
2022-01-31 10:33:21 +01:00
|
|
|
|
k = self.__check_name(k)
|
|
|
|
|
v = self.__check_value(v)
|
2022-01-31 09:38:14 +01:00
|
|
|
|
self.__set(k, v)
|
2022-03-31 10:34:51 +02:00
|
|
|
|
|
|
|
|
|
|
2022-05-02 12:06:19 +02:00
|
|
|
|
def to_name(self, k):
|
|
|
|
|
if k == None:
|
|
|
|
|
k = 0
|
|
|
|
|
return self.name(k)
|
|
|
|
|
|
|
|
|
|
|
2022-03-31 10:34:51 +02:00
|
|
|
|
def __alias(self, k, *args):
|
|
|
|
|
v = 0
|
|
|
|
|
for a in args:
|
|
|
|
|
a = self.__check_value_cursor(a)
|
|
|
|
|
v = self.__check_limit(v | a, pure=False)
|
2022-05-06 09:18:59 +02:00
|
|
|
|
if self.is_pure(v):
|
2022-03-31 10:34:51 +02:00
|
|
|
|
raise ValueError('use add to add pure values')
|
|
|
|
|
return self.__set(k, v)
|
|
|
|
|
|
2022-01-31 09:38:14 +01:00
|
|
|
|
|
2022-02-06 09:11:47 +01:00
|
|
|
|
def alias(self, k, *args):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Add an alias for a combination of states in the store.
|
|
|
|
|
|
|
|
|
|
State aggregates may be provided as comma separated values or as a single (or'd) integer value.
|
|
|
|
|
|
|
|
|
|
:param k: Alias name
|
|
|
|
|
:type k: str
|
|
|
|
|
:param *args: One or more states to aggregate for this alias.
|
|
|
|
|
:type *args: int or list of ints
|
|
|
|
|
:raises StateInvalid: Attempt to create alias for one or more atomic states that do not exist.
|
|
|
|
|
:raises ValueError: Attempt to use bit value as alias
|
|
|
|
|
"""
|
2022-01-31 10:33:21 +01:00
|
|
|
|
k = self.__check_name(k)
|
2022-03-31 10:34:51 +02:00
|
|
|
|
return self.__alias(k, *args)
|
2022-01-31 09:38:14 +01:00
|
|
|
|
|
|
|
|
|
|
2022-05-02 13:21:07 +02:00
|
|
|
|
def all(self, pure=False, numeric=False):
|
|
|
|
|
"""Return list of all unique atomic and alias state strings.
|
2022-02-09 17:47:29 +01:00
|
|
|
|
|
|
|
|
|
:rtype: list of ints
|
|
|
|
|
:return: states
|
|
|
|
|
"""
|
2022-01-31 10:12:49 +01:00
|
|
|
|
l = []
|
|
|
|
|
for k in dir(self):
|
2022-05-02 13:21:07 +02:00
|
|
|
|
state = None
|
2022-01-31 10:12:49 +01:00
|
|
|
|
if k[0] == '_':
|
|
|
|
|
continue
|
|
|
|
|
if k.upper() != k:
|
|
|
|
|
continue
|
2022-03-17 22:36:07 +01:00
|
|
|
|
if pure:
|
|
|
|
|
state = self.from_name(k)
|
2022-05-06 09:18:59 +02:00
|
|
|
|
if not self.is_pure(state):
|
2022-03-17 22:36:07 +01:00
|
|
|
|
continue
|
2022-05-02 13:21:07 +02:00
|
|
|
|
if numeric:
|
|
|
|
|
if state == None:
|
|
|
|
|
state = self.from_name(k)
|
|
|
|
|
l.append(state)
|
|
|
|
|
else:
|
|
|
|
|
l.append(k)
|
2022-01-31 10:12:49 +01:00
|
|
|
|
l.sort()
|
|
|
|
|
return l
|
2022-01-31 10:33:21 +01:00
|
|
|
|
|
|
|
|
|
|
2022-05-06 09:18:59 +02:00
|
|
|
|
def elements(self, v, numeric=False, as_string=True):
|
2022-03-16 17:49:00 +01:00
|
|
|
|
r = []
|
|
|
|
|
if v == None or v == 0:
|
|
|
|
|
return self.base_state_name
|
|
|
|
|
c = 1
|
2022-03-29 13:29:10 +02:00
|
|
|
|
for i in range(self.__bits):
|
2022-03-16 17:49:00 +01:00
|
|
|
|
if v & c > 0:
|
2022-05-06 09:18:59 +02:00
|
|
|
|
if numeric:
|
|
|
|
|
r.append(c)
|
|
|
|
|
else:
|
|
|
|
|
r.append(self.name(c))
|
2022-03-16 17:49:00 +01:00
|
|
|
|
c <<= 1
|
2022-05-06 09:18:59 +02:00
|
|
|
|
|
|
|
|
|
if numeric or not as_string:
|
|
|
|
|
return r
|
|
|
|
|
|
2022-03-31 10:34:51 +02:00
|
|
|
|
return '_' + '.'.join(r)
|
2022-03-24 00:34:13 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def from_elements(self, k):
|
|
|
|
|
r = 0
|
|
|
|
|
if k[0] != '_':
|
|
|
|
|
raise ValueError('elements string must start with underscore (_), got {}'.format(k))
|
2022-03-31 10:34:51 +02:00
|
|
|
|
for v in k[1:].split('.'):
|
2022-03-24 00:34:13 +01:00
|
|
|
|
r |= self.from_name(v)
|
|
|
|
|
return r
|
2022-03-16 17:49:00 +01:00
|
|
|
|
|
|
|
|
|
|
2022-01-31 13:10:04 +01:00
|
|
|
|
def name(self, v):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Retrieve that string representation of the state attribute represented by the given state integer value.
|
|
|
|
|
|
|
|
|
|
:param v: State integer
|
|
|
|
|
:type v: int
|
|
|
|
|
:raises StateInvalid: State corresponding to given integer not found
|
|
|
|
|
:rtype: str
|
|
|
|
|
:return: State name
|
|
|
|
|
"""
|
2022-01-31 13:10:04 +01:00
|
|
|
|
k = self.__reverse.get(v)
|
|
|
|
|
if k == None:
|
2022-03-16 17:49:00 +01:00
|
|
|
|
if self.check_alias:
|
|
|
|
|
raise StateInvalid(v)
|
|
|
|
|
else:
|
|
|
|
|
k = self.elements(v)
|
|
|
|
|
elif v == None or v == 0:
|
|
|
|
|
return self.base_state_name
|
2022-01-31 13:10:04 +01:00
|
|
|
|
return k
|
|
|
|
|
|
|
|
|
|
|
2022-02-03 11:05:59 +01:00
|
|
|
|
def from_name(self, k):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Retrieve the real state integer value corresponding to an attribute name.
|
|
|
|
|
|
|
|
|
|
:param k: Attribute name
|
|
|
|
|
:type k: str
|
|
|
|
|
:raises ValueError: Invalid attribute name
|
|
|
|
|
:raises AttributeError: Attribute not found
|
|
|
|
|
:rtype: int
|
|
|
|
|
:return: Numeric state value
|
|
|
|
|
"""
|
2022-02-03 11:05:59 +01:00
|
|
|
|
k = self.__check_name_valid(k)
|
|
|
|
|
return getattr(self, k)
|
|
|
|
|
|
|
|
|
|
|
2022-01-31 10:55:56 +01:00
|
|
|
|
def match(self, v, pure=False):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Match against all stored states.
|
|
|
|
|
|
|
|
|
|
If pure is set, only match against the single atomic state will be returned.
|
|
|
|
|
|
|
|
|
|
:param v: Integer state to match
|
|
|
|
|
:type v: int
|
|
|
|
|
:param pure: Match only pure states
|
|
|
|
|
:type pure: bool
|
|
|
|
|
:raises KeyError: Unknown state
|
|
|
|
|
:rtype: tuple
|
|
|
|
|
:return: 0: Alias that input resolves to, 1: list of atomic states that matches the state
|
|
|
|
|
"""
|
2022-01-31 11:06:20 +01:00
|
|
|
|
alias = None
|
2022-01-31 10:55:56 +01:00
|
|
|
|
if not pure:
|
2022-01-31 11:06:20 +01:00
|
|
|
|
alias = self.__reverse.get(v)
|
|
|
|
|
|
|
|
|
|
r = []
|
2022-01-31 10:33:21 +01:00
|
|
|
|
c = 1
|
|
|
|
|
for i in range(self.__bits):
|
|
|
|
|
if v & c > 0:
|
2022-01-31 10:55:56 +01:00
|
|
|
|
try:
|
|
|
|
|
k = self.__reverse[c]
|
|
|
|
|
r.append(k)
|
|
|
|
|
except KeyError:
|
|
|
|
|
pass
|
2022-01-31 10:33:21 +01:00
|
|
|
|
c <<= 1
|
|
|
|
|
|
2022-01-31 11:06:20 +01:00
|
|
|
|
return (alias, r,)
|
2022-01-31 12:23:51 +01:00
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
|
|
|
|
|
def put(self, key, state=None, contents=None):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Add a key to an existing state.
|
|
|
|
|
|
2022-02-09 20:20:59 +01:00
|
|
|
|
If no state it specified, the default state attribute State.base_state_name will be used.
|
2022-02-09 17:47:29 +01:00
|
|
|
|
|
|
|
|
|
Contents may be supplied as value to pair with the given key. Contents may be changed later by calling the `replace` method.
|
|
|
|
|
|
|
|
|
|
:param key: Content key to add
|
|
|
|
|
:type key: str
|
2022-02-09 20:20:59 +01:00
|
|
|
|
:param state: Initial state for the put. If not given, initial state will be State.base_state_name
|
2022-02-09 17:47:29 +01:00
|
|
|
|
:type state: int
|
|
|
|
|
:param contents: Contents to associate with key. A valie of None should be recognized as an undefined value as opposed to a zero-length value throughout any backend
|
|
|
|
|
:type contents: str
|
|
|
|
|
:raises StateItemExists: Content key has already been added
|
|
|
|
|
:raises StateInvalid: Given state has not been registered
|
|
|
|
|
:rtype: integer
|
|
|
|
|
:return: Resulting state that key is put under (should match the input state)
|
|
|
|
|
"""
|
2022-01-31 12:23:51 +01:00
|
|
|
|
if state == None:
|
2022-02-09 20:20:59 +01:00
|
|
|
|
state = getattr(self, self.base_state_name)
|
2022-03-17 20:16:33 +01:00
|
|
|
|
elif self.__reverse.get(state) == None and self.check_alias:
|
2022-01-31 12:23:51 +01:00
|
|
|
|
raise StateInvalid(state)
|
2022-02-07 20:47:06 +01:00
|
|
|
|
self.__check_key(key)
|
2022-03-24 00:34:13 +01:00
|
|
|
|
|
|
|
|
|
if self.event_callback != None:
|
|
|
|
|
old_state = self.__keys_reverse.get(key)
|
2022-03-29 11:24:57 +02:00
|
|
|
|
self.event_callback(key, None, self.name(state))
|
2022-03-24 00:34:13 +01:00
|
|
|
|
|
2022-02-01 07:29:51 +01:00
|
|
|
|
self.__add_state_list(state, key)
|
2022-02-01 10:34:36 +01:00
|
|
|
|
if contents != None:
|
|
|
|
|
self.__contents[key] = contents
|
2022-02-01 14:54:27 +01:00
|
|
|
|
|
2022-03-13 17:36:17 +01:00
|
|
|
|
self.register_modify(key)
|
|
|
|
|
|
2022-02-01 14:54:27 +01:00
|
|
|
|
return state
|
2022-01-31 12:23:51 +01:00
|
|
|
|
|
|
|
|
|
|
2022-02-01 07:29:51 +01:00
|
|
|
|
def move(self, key, to_state):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Move a given content key from one state to another.
|
|
|
|
|
|
|
|
|
|
:param key: Key to move
|
|
|
|
|
:type key: str
|
|
|
|
|
:param to_state: Numeric state to move to (may be atomic or alias)
|
|
|
|
|
:type to_state: integer
|
|
|
|
|
:raises StateItemNotFound: Given key has not been registered
|
|
|
|
|
:raises StateInvalid: Given state has not been registered
|
|
|
|
|
:rtype: integer
|
|
|
|
|
:return: Resulting state from move (should match the state given as input)
|
|
|
|
|
"""
|
2022-02-01 07:29:51 +01:00
|
|
|
|
current_state = self.__keys_reverse.get(key)
|
2022-01-31 12:23:51 +01:00
|
|
|
|
if current_state == None:
|
2022-02-01 07:29:51 +01:00
|
|
|
|
raise StateItemNotFound(key)
|
2022-01-31 12:23:51 +01:00
|
|
|
|
|
|
|
|
|
new_state = self.__reverse.get(to_state)
|
2022-03-17 22:36:07 +01:00
|
|
|
|
if new_state == None and self.check_alias:
|
2022-01-31 12:23:51 +01:00
|
|
|
|
raise StateInvalid(to_state)
|
|
|
|
|
|
2022-02-01 14:54:27 +01:00
|
|
|
|
return self.__move(key, current_state, to_state)
|
2022-02-01 08:47:07 +01:00
|
|
|
|
|
|
|
|
|
|
2022-02-06 21:43:20 +01:00
|
|
|
|
# implementation for state move that ensures integrity of keys and states.
|
2022-02-01 08:47:07 +01:00
|
|
|
|
def __move(self, key, from_state, to_state):
|
|
|
|
|
current_state_list = self.__keys.get(from_state)
|
2022-01-31 12:23:51 +01:00
|
|
|
|
if current_state_list == None:
|
|
|
|
|
raise StateCorruptionError(current_state)
|
|
|
|
|
|
2022-02-01 07:29:51 +01:00
|
|
|
|
idx = self.__state_list_index(key, current_state_list)
|
2022-01-31 12:23:51 +01:00
|
|
|
|
|
2022-02-01 07:29:51 +01:00
|
|
|
|
new_state_list = self.__keys.get(to_state)
|
2022-01-31 12:23:51 +01:00
|
|
|
|
if current_state_list == None:
|
|
|
|
|
raise StateCorruptionError(to_state)
|
|
|
|
|
|
2022-03-11 11:31:08 +01:00
|
|
|
|
if self.verifier != None:
|
|
|
|
|
r = self.verifier(self, from_state, to_state)
|
|
|
|
|
if r != None:
|
2022-03-11 13:01:56 +01:00
|
|
|
|
raise StateTransitionInvalid(r)
|
2022-03-11 11:31:08 +01:00
|
|
|
|
|
2022-01-31 12:23:51 +01:00
|
|
|
|
current_state_list.pop(idx)
|
2022-03-24 00:34:13 +01:00
|
|
|
|
|
|
|
|
|
if self.event_callback != None:
|
|
|
|
|
old_state = self.__keys_reverse.get(key)
|
|
|
|
|
self.event_callback(key, self.name(old_state), self.name(to_state))
|
|
|
|
|
|
2022-03-11 20:36:24 +01:00
|
|
|
|
self.__add_state_list(to_state, key)
|
2022-01-31 12:23:51 +01:00
|
|
|
|
|
2022-03-13 17:36:17 +01:00
|
|
|
|
self.register_modify(key)
|
|
|
|
|
|
2022-02-01 14:54:27 +01:00
|
|
|
|
return to_state
|
2022-02-06 21:43:20 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set(self, key, or_state):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Move to an alias state by setting a single bit.
|
|
|
|
|
|
|
|
|
|
:param key: Content key to modify state for
|
|
|
|
|
:type key: str
|
|
|
|
|
:param or_state: Atomic stat to add
|
|
|
|
|
:type or_state: int
|
|
|
|
|
:raises ValueError: State is not a single bit state
|
|
|
|
|
:raises StateItemNotFound: Content key is not registered
|
|
|
|
|
:raises StateInvalid: Resulting state after addition of atomic state is unknown
|
|
|
|
|
:rtype: int
|
|
|
|
|
:returns: Resulting state
|
|
|
|
|
"""
|
2022-05-06 09:18:59 +02:00
|
|
|
|
if not self.is_pure(or_state):
|
2022-02-01 08:47:07 +01:00
|
|
|
|
raise ValueError('can only apply using single bit states')
|
|
|
|
|
|
|
|
|
|
current_state = self.__keys_reverse.get(key)
|
|
|
|
|
if current_state == None:
|
|
|
|
|
raise StateItemNotFound(key)
|
|
|
|
|
|
|
|
|
|
to_state = current_state | or_state
|
|
|
|
|
new_state = self.__reverse.get(to_state)
|
2022-03-16 17:49:00 +01:00
|
|
|
|
if new_state == None and self.check_alias:
|
2022-02-01 08:47:07 +01:00
|
|
|
|
raise StateInvalid('resulting to state is unknown: {}'.format(to_state))
|
|
|
|
|
|
2022-02-01 14:54:27 +01:00
|
|
|
|
return self.__move(key, current_state, to_state)
|
2022-02-01 08:47:07 +01:00
|
|
|
|
|
2022-03-11 13:01:56 +01:00
|
|
|
|
|
2022-05-02 12:06:19 +02:00
|
|
|
|
def unset(self, key, not_state, allow_base=False):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Unset a single bit, moving to a pure or alias state.
|
|
|
|
|
|
2022-02-09 20:20:59 +01:00
|
|
|
|
The resulting state cannot be State.base_state_name (0).
|
2022-02-09 17:47:29 +01:00
|
|
|
|
|
|
|
|
|
:param key: Content key to modify state for
|
|
|
|
|
:type key: str
|
|
|
|
|
:param or_state: Atomic stat to add
|
|
|
|
|
:type or_state: int
|
2022-02-09 20:20:59 +01:00
|
|
|
|
:raises ValueError: State is not a single bit state, or attempts to revert to State.base_state_name
|
2022-02-09 17:47:29 +01:00
|
|
|
|
:raises StateItemNotFound: Content key is not registered
|
|
|
|
|
:raises StateInvalid: Resulting state after addition of atomic state is unknown
|
|
|
|
|
:rtype: int
|
|
|
|
|
:returns: Resulting state
|
|
|
|
|
"""
|
2022-05-06 09:18:59 +02:00
|
|
|
|
if not self.is_pure(not_state):
|
2022-02-01 08:47:07 +01:00
|
|
|
|
raise ValueError('can only apply using single bit states')
|
|
|
|
|
|
|
|
|
|
current_state = self.__keys_reverse.get(key)
|
|
|
|
|
if current_state == None:
|
|
|
|
|
raise StateItemNotFound(key)
|
|
|
|
|
|
|
|
|
|
to_state = current_state & (~not_state)
|
|
|
|
|
if to_state == current_state:
|
|
|
|
|
raise ValueError('invalid change for state {}: {}'.format(key, not_state))
|
|
|
|
|
|
2022-05-02 12:06:19 +02:00
|
|
|
|
if to_state == getattr(self, self.base_state_name) and not allow_base:
|
2022-02-09 20:20:59 +01:00
|
|
|
|
raise ValueError('State {} for {} cannot be reverted to {}'.format(current_state, key, self.base_state_name))
|
2022-02-07 20:47:06 +01:00
|
|
|
|
|
2022-02-01 08:47:07 +01:00
|
|
|
|
new_state = self.__reverse.get(to_state)
|
|
|
|
|
if new_state == None:
|
|
|
|
|
raise StateInvalid('resulting to state is unknown: {}'.format(to_state))
|
|
|
|
|
|
2022-02-01 14:54:27 +01:00
|
|
|
|
return self.__move(key, current_state, to_state)
|
2022-02-01 08:47:07 +01:00
|
|
|
|
|
|
|
|
|
|
2022-03-11 13:01:56 +01:00
|
|
|
|
def change(self, key, sets, unsets):
|
|
|
|
|
current_state = self.__keys_reverse.get(key)
|
|
|
|
|
if current_state == None:
|
|
|
|
|
raise StateItemNotFound(key)
|
|
|
|
|
to_state = current_state | sets
|
|
|
|
|
to_state &= ~unsets & self.__limit
|
|
|
|
|
|
|
|
|
|
if sets == 0:
|
|
|
|
|
to_state = current_state & (~unsets)
|
|
|
|
|
if to_state == current_state:
|
|
|
|
|
raise ValueError('invalid change by unsets for state {}: {}'.format(key, unsets))
|
|
|
|
|
|
|
|
|
|
if to_state == getattr(self, self.base_state_name):
|
|
|
|
|
raise ValueError('State {} for {} cannot be reverted to {}'.format(current_state, key, self.base_state_name))
|
|
|
|
|
|
|
|
|
|
new_state = self.__reverse.get(to_state)
|
|
|
|
|
if new_state == None:
|
|
|
|
|
raise StateInvalid('resulting to state is unknown: {}'.format(to_state))
|
|
|
|
|
|
|
|
|
|
return self.__move(key, current_state, to_state)
|
|
|
|
|
|
|
|
|
|
|
2022-02-01 07:29:51 +01:00
|
|
|
|
def state(self, key):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Return the current numeric state for the given content key.
|
|
|
|
|
|
|
|
|
|
:param key: Key to return content for
|
|
|
|
|
:type key: str
|
|
|
|
|
:raises StateItemNotFound: Content key is unknown
|
|
|
|
|
:rtype: int
|
|
|
|
|
:returns: State
|
|
|
|
|
"""
|
2022-02-01 07:29:51 +01:00
|
|
|
|
state = self.__keys_reverse.get(key)
|
2022-01-31 12:23:51 +01:00
|
|
|
|
if state == None:
|
2022-02-01 07:29:51 +01:00
|
|
|
|
raise StateItemNotFound(key)
|
2022-01-31 12:23:51 +01:00
|
|
|
|
return state
|
2022-02-01 08:01:57 +01:00
|
|
|
|
|
|
|
|
|
|
2022-02-07 20:47:06 +01:00
|
|
|
|
def get(self, key):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Retrieve the content for a content key.
|
|
|
|
|
|
|
|
|
|
:param key: Content key to retrieve content for
|
|
|
|
|
:type key: str
|
|
|
|
|
:rtype: any
|
|
|
|
|
:returns: Content
|
|
|
|
|
"""
|
2022-02-01 10:34:36 +01:00
|
|
|
|
return self.__contents.get(key)
|
2022-02-01 10:55:12 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def list(self, state):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""List all content keys matching a state.
|
|
|
|
|
|
|
|
|
|
:param state: State to match
|
|
|
|
|
:type state: int
|
|
|
|
|
:rtype: list of str
|
|
|
|
|
:returns: Matching content keys
|
|
|
|
|
"""
|
2022-02-04 23:15:51 +01:00
|
|
|
|
try:
|
|
|
|
|
return self.__keys[state]
|
|
|
|
|
except KeyError:
|
|
|
|
|
return []
|
2022-02-02 08:59:52 +01:00
|
|
|
|
|
|
|
|
|
|
2022-03-17 23:01:51 +01:00
|
|
|
|
def sync(self, state=None):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Noop method for interface implementation providing sync to backend.
|
|
|
|
|
|
|
|
|
|
:param state: State to sync.
|
|
|
|
|
:type state:
|
|
|
|
|
:todo: (for higher level implementer) if sync state is none, sync all
|
|
|
|
|
"""
|
2022-02-02 10:27:57 +01:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def path(self, state, key=None):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""In the memory-only class no persisted state is used, and this will return None.
|
|
|
|
|
|
|
|
|
|
See shep.persist.PersistedState.path for more information.
|
|
|
|
|
"""
|
2022-02-02 10:27:57 +01:00
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
2022-02-02 08:59:52 +01:00
|
|
|
|
def peek(self, key):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Return the next pure state.
|
|
|
|
|
|
|
|
|
|
Will return the same result as the method next, but without advancing to the new state.
|
|
|
|
|
|
|
|
|
|
:param key: Content key to inspect state for
|
|
|
|
|
:type key: str
|
|
|
|
|
:raises StateItemNotFound: Unknown content key
|
|
|
|
|
:raises StateInvalid: Attempt to advance from an alias state, OR beyond the last known pure state.
|
|
|
|
|
:rtype: int
|
|
|
|
|
:returns: Next state
|
|
|
|
|
"""
|
2022-02-02 08:59:52 +01:00
|
|
|
|
state = self.__keys_reverse.get(key)
|
|
|
|
|
if state == None:
|
|
|
|
|
raise StateItemNotFound(key)
|
2022-05-06 09:18:59 +02:00
|
|
|
|
if not self.is_pure(state):
|
2022-02-02 08:59:52 +01:00
|
|
|
|
raise StateInvalid('cannot run next on an alias state')
|
|
|
|
|
|
|
|
|
|
if state == 0:
|
|
|
|
|
state = 1
|
|
|
|
|
else:
|
|
|
|
|
state <<= 1
|
2022-03-17 22:36:07 +01:00
|
|
|
|
if state > self.__limit:
|
2022-02-02 08:59:52 +01:00
|
|
|
|
raise StateInvalid('unknown state {}'.format(state))
|
|
|
|
|
|
|
|
|
|
return state
|
2022-02-02 10:27:57 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def next(self, key):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Advance to the next pure state.
|
|
|
|
|
|
|
|
|
|
:param key: Content key to inspect state for
|
|
|
|
|
:type key: str
|
|
|
|
|
:raises StateItemNotFound: Unknown content key
|
|
|
|
|
:raises StateInvalid: Attempt to advance from an alias state, OR beyond the last known pure state.
|
|
|
|
|
:rtype: int
|
|
|
|
|
:returns: Next state
|
|
|
|
|
"""
|
2022-02-02 10:27:57 +01:00
|
|
|
|
from_state = self.state(key)
|
|
|
|
|
new_state = self.peek(key)
|
|
|
|
|
return self.__move(key, from_state, new_state)
|
2022-02-04 23:15:51 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def replace(self, key, contents):
|
2022-02-09 17:47:29 +01:00
|
|
|
|
"""Replace contents associated by content key.
|
|
|
|
|
|
|
|
|
|
:param key: Content key to replace for
|
|
|
|
|
:type key: str
|
|
|
|
|
:param contents: New contents
|
|
|
|
|
:type contents: any
|
|
|
|
|
:raises KeyError: Unknown content key
|
|
|
|
|
"""
|
2022-02-04 23:15:51 +01:00
|
|
|
|
self.state(key)
|
|
|
|
|
self.__contents[key] = contents
|
2022-03-13 17:36:17 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def modified(self, key):
|
2022-03-16 17:49:00 +01:00
|
|
|
|
return self.modified_last[key]
|
2022-03-13 17:36:17 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def register_modify(self, key):
|
2022-04-09 19:19:48 +02:00
|
|
|
|
self.modified_last[key] = datetime.datetime.utcnow().timestamp()
|
2022-03-18 19:34:49 +01:00
|
|
|
|
|
|
|
|
|
|
2022-03-24 00:34:13 +01:00
|
|
|
|
def mask(self, key, states=0):
|
2022-03-18 19:34:49 +01:00
|
|
|
|
statemask = self.__limit + 1
|
|
|
|
|
statemask |= states
|
|
|
|
|
statemask = ~statemask
|
|
|
|
|
statemask &= self.__limit
|
|
|
|
|
return statemask
|
2022-05-02 13:21:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def purge(self, key):
|
|
|
|
|
state = self.state(key)
|
|
|
|
|
state_name = self.name(state)
|
|
|
|
|
|
|
|
|
|
v = self.__keys.get(state)
|
|
|
|
|
v.remove(key)
|
|
|
|
|
|
|
|
|
|
del self.__keys_reverse[key]
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
del self.__contents[key]
|
|
|
|
|
except KeyError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
del self.modified_last[key]
|
|
|
|
|
except KeyError:
|
|
|
|
|
pass
|