WIP add docstrings to persist

This commit is contained in:
lash 2022-02-09 16:47:29 +00:00
parent dbb2280a03
commit 5cc0af80d6
Signed by: lash
GPG Key ID: 21D2E7BB88C2A746
2 changed files with 229 additions and 161 deletions

View File

@ -4,6 +4,15 @@ from .error import StateItemExists
class PersistedState(State): class PersistedState(State):
"""Adapter for persisting state changes and synchronising states between memory and persisted backend.
:param factory: A function capable of returning a persisted store from a single path argument.
:type factory: function
:param bits: Number of pure states. Passed to the superclass.
:type bits: int
:param logger: Logger to capture logging output, or None for no logging.
:type logger: object
"""
def __init__(self, factory, bits, logger=None): def __init__(self, factory, bits, logger=None):
super(PersistedState, self).__init__(bits, logger=logger) super(PersistedState, self).__init__(bits, logger=logger)
@ -11,12 +20,17 @@ class PersistedState(State):
self.__stores = {} self.__stores = {}
# Create state store container if missing.
def __ensure_store(self, k): def __ensure_store(self, k):
if self.__stores.get(k) == None: if self.__stores.get(k) == None:
self.__stores[k] = self.__store_factory(k) self.__stores[k] = self.__store_factory(k)
def put(self, key, contents=None, state=None): def put(self, key, contents=None, state=None):
"""Persist a key or key/content pair.
See shep.state.State.put
"""
to_state = super(PersistedState, self).put(key, state=state, contents=contents) to_state = super(PersistedState, self).put(key, state=state, contents=contents)
k = self.name(to_state) k = self.name(to_state)
@ -26,6 +40,10 @@ class PersistedState(State):
def set(self, key, or_state): def set(self, key, or_state):
"""Persist a new state for a key or key/content.
See shep.state.State.set
"""
from_state = self.state(key) from_state = self.state(key)
k_from = self.name(from_state) k_from = self.name(from_state)
@ -41,6 +59,10 @@ class PersistedState(State):
def unset(self, key, not_state): def unset(self, key, not_state):
"""Persist a new state for a key or key/content.
See shep.state.State.unset
"""
from_state = self.state(key) from_state = self.state(key)
k_from = self.name(from_state) k_from = self.name(from_state)
@ -57,11 +79,16 @@ class PersistedState(State):
def move(self, key, to_state): def move(self, key, to_state):
"""Persist a new state for a key or key/content.
See shep.state.State.move
"""
from_state = self.state(key) from_state = self.state(key)
to_state = super(PersistedState, self).move(key, to_state) to_state = super(PersistedState, self).move(key, to_state)
return self.__movestore(key, from_state, to_state) return self.__movestore(key, from_state, to_state)
# common procedure for safely moving a persisted resource from one state to another.
def __movestore(self, key, from_state, to_state): def __movestore(self, key, from_state, to_state):
k_from = self.name(from_state) k_from = self.name(from_state)
k_to = self.name(to_state) k_to = self.name(to_state)
@ -76,6 +103,13 @@ class PersistedState(State):
def sync(self, state): def sync(self, state):
"""Reload resources for a single state in memory from the persisted state store.
:param state: State to load
:type state: int
:raises StateItemExists: A content key is already recorded with a different state in memory than in persisted store.
# :todo: if sync state is none, sync all
"""
k = self.name(state) k = self.name(state)
self.__ensure_store(k) self.__ensure_store(k)
@ -89,36 +123,51 @@ class PersistedState(State):
def list(self, state): def list(self, state):
"""List all content keys for a particular state.
This method will return from memory, and will not sync the persisted state first.
See shep.state.State.list
"""
k = self.name(state) k = self.name(state)
self.__ensure_store(k) self.__ensure_store(k)
#return self.__stores[k].list(state) #return self.__stores[k].list(state)
return super(PersistedState, self).list(state) return super(PersistedState, self).list(state)
# Return a file path or URL pointing to the persisted state.
#
# If the key is omitted, the URL to the state item's container must be returned, and None if no such container exists.
#
# :param state: State to locate
# :type state: int
# :param key: Content key to locate
# :type key: str
# :rtype: str
# :returns: Locator pointng to persisted state
# :todo: rename to "location"
def path(self, state, key=None): def path(self, state, key=None):
"""Return a file path or URL pointing to the persisted state.
If the key is omitted, the URL to the state item's container must be returned, and None if no such container exists.
:param state: State to locate
:type state: int
:param key: Content key to locate
:type key: str
:rtype: str
:returns: Locator pointng to persisted state
:todo: rename to "location"
"""
k = self.name(state) k = self.name(state)
self.__ensure_store(k) self.__ensure_store(k)
return self.__stores[k].path(key=key) return self.__stores[k].path(key=key)
def next(self, key=None): def next(self, key=None):
"""Advance and persist to the next pure state.
See shep.state.State.next
"""
from_state = self.state(key) from_state = self.state(key)
to_state = super(PersistedState, self).next(key) to_state = super(PersistedState, self).next(key)
return self.__movestore(key, from_state, to_state) return self.__movestore(key, from_state, to_state)
def replace(self, key, contents): def replace(self, key, contents):
"""Replace contents associated by content key.
See shep.state.State.replace
"""
super(PersistedState, self).replace(key, contents) super(PersistedState, self).replace(key, contents)
state = self.state(key) state = self.state(key)
k = self.name(state) k = self.name(state)

View File

@ -118,10 +118,11 @@ class State:
self.__keys_reverse[item] = state self.__keys_reverse[item] = state
# 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)
def __state_list_index(self, item, state_list): def __state_list_index(self, item, state_list):
"""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)
"""
idx = -1 idx = -1
try: try:
idx = state_list.index(item) idx = state_list.index(item)
@ -134,29 +135,31 @@ class State:
return idx return idx
# Add a state to the store.
#
# :param k: State name
# :type k: str
# :raises shep.error.StateExists: State name is already registered
def add(self, k): def add(self, k):
"""Add a state to the store.
:param k: State name
:type k: str
:raises shep.error.StateExists: State name is already registered
"""
v = 1 << self.__c v = 1 << self.__c
k = self.__check_name(k) k = self.__check_name(k)
v = self.__check_value(v) v = self.__check_value(v)
self.__set(k, v) self.__set(k, v)
# 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
def alias(self, k, *args): def alias(self, k, *args):
"""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
"""
k = self.__check_name(k) k = self.__check_name(k)
v = 0 v = 0
for a in args: for a in args:
@ -167,11 +170,12 @@ class State:
self.__set(k, v) self.__set(k, v)
# Return list of all unique atomic and alias states.
#
# :rtype: list of ints
# :return: states
def all(self): def all(self):
"""Return list of all unique atomic and alias states.
:rtype: list of ints
:return: states
"""
l = [] l = []
for k in dir(self): for k in dir(self):
if k[0] == '_': if k[0] == '_':
@ -183,14 +187,15 @@ class State:
return l return l
# 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
def name(self, v): def name(self, v):
"""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
"""
if v == None or v == 0: if v == None or v == 0:
return 'NEW' return 'NEW'
k = self.__reverse.get(v) k = self.__reverse.get(v)
@ -199,31 +204,33 @@ class State:
return k return k
# 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
def from_name(self, k): def from_name(self, k):
"""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
"""
k = self.__check_name_valid(k) k = self.__check_name_valid(k)
return getattr(self, k) return getattr(self, k)
# 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
def match(self, v, pure=False): def match(self, v, pure=False):
"""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
"""
alias = None alias = None
if not pure: if not pure:
alias = self.__reverse.get(v) alias = self.__reverse.get(v)
@ -242,23 +249,24 @@ class State:
return (alias, r,) return (alias, r,)
# Add a key to an existing state.
#
# If no state it specified, the default state attribute "NEW" will be used.
#
# 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
# :param state: Initial state for the put. If not given, initial state will be NEW
# :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)
def put(self, key, state=None, contents=None): def put(self, key, state=None, contents=None):
"""Add a key to an existing state.
If no state it specified, the default state attribute "NEW" will be used.
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
:param state: Initial state for the put. If not given, initial state will be NEW
: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)
"""
if state == None: if state == None:
state = self.NEW state = self.NEW
elif self.__reverse.get(state) == None: elif self.__reverse.get(state) == None:
@ -271,17 +279,18 @@ class State:
return state return state
# 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)
def move(self, key, to_state): def move(self, key, to_state):
"""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)
"""
current_state = self.__keys_reverse.get(key) current_state = self.__keys_reverse.get(key)
if current_state == None: if current_state == None:
raise StateItemNotFound(key) raise StateItemNotFound(key)
@ -311,18 +320,19 @@ class State:
return to_state return to_state
# 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
def set(self, key, or_state): def set(self, key, or_state):
"""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
"""
if not self.__is_pure(or_state): if not self.__is_pure(or_state):
raise ValueError('can only apply using single bit states') raise ValueError('can only apply using single bit states')
@ -338,20 +348,21 @@ class State:
return self.__move(key, current_state, to_state) return self.__move(key, current_state, to_state)
# Unset a single bit, moving to a pure or alias state.
#
# The resulting state cannot be NEW (0).
#
# :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, or attempts to revert to NEW
# :raises StateItemNotFound: Content key is not registered
# :raises StateInvalid: Resulting state after addition of atomic state is unknown
# :rtype: int
# :returns: Resulting state
def unset(self, key, not_state): def unset(self, key, not_state):
"""Unset a single bit, moving to a pure or alias state.
The resulting state cannot be NEW (0).
: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, or attempts to revert to NEW
:raises StateItemNotFound: Content key is not registered
:raises StateInvalid: Resulting state after addition of atomic state is unknown
:rtype: int
:returns: Resulting state
"""
if not self.__is_pure(not_state): if not self.__is_pure(not_state):
raise ValueError('can only apply using single bit states') raise ValueError('can only apply using single bit states')
@ -373,70 +384,76 @@ class State:
return self.__move(key, current_state, to_state) return self.__move(key, current_state, to_state)
# 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
def state(self, key): def state(self, key):
"""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
"""
state = self.__keys_reverse.get(key) state = self.__keys_reverse.get(key)
if state == None: if state == None:
raise StateItemNotFound(key) raise StateItemNotFound(key)
return state return state
# Retrieve the content for a content key.
#
# :param key: Content key to retrieve content for
# :type key: str
# :rtype: any
# :returns: Content
def get(self, key): def get(self, key):
"""Retrieve the content for a content key.
:param key: Content key to retrieve content for
:type key: str
:rtype: any
:returns: Content
"""
return self.__contents.get(key) return self.__contents.get(key)
# List all content keys matching a state.
#
# :param state: State to match
# :type state: int
# :rtype: list of str
# :returns: Matching content keys
def list(self, state): def list(self, state):
"""List all content keys matching a state.
:param state: State to match
:type state: int
:rtype: list of str
:returns: Matching content keys
"""
try: try:
return self.__keys[state] return self.__keys[state]
except KeyError: except KeyError:
return [] return []
# 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
def sync(self, state): def sync(self, state):
"""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
"""
pass pass
# In the memory-only class no persisted state is used, and this will return None.
#
# See shep.persist.PersistedState.path for more information.
def path(self, state, key=None): def path(self, state, key=None):
"""In the memory-only class no persisted state is used, and this will return None.
See shep.persist.PersistedState.path for more information.
"""
return None return None
# 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
def peek(self, key): def peek(self, key):
"""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
"""
state = self.__keys_reverse.get(key) state = self.__keys_reverse.get(key)
if state == None: if state == None:
raise StateItemNotFound(key) raise StateItemNotFound(key)
@ -453,27 +470,29 @@ class State:
return state return state
# 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
def next(self, key): def next(self, key):
"""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
"""
from_state = self.state(key) from_state = self.state(key)
new_state = self.peek(key) new_state = self.peek(key)
return self.__move(key, from_state, new_state) return self.__move(key, from_state, new_state)
# 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
def replace(self, key, contents): def replace(self, key, contents):
"""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
"""
self.state(key) self.state(key)
self.__contents[key] = contents self.__contents[key] = contents