shep

Multi-state key stores using bit masks for python3
git clone git://git.defalsify.org/shep.git
Log | Files | Refs | LICENSE

commit 5cc0af80d64381084a72be73632f6e4fb8b7ba96
parent dbb2280a034bea0954e95945bee4820e5b873c85
Author: lash <dev@holbrook.no>
Date:   Wed,  9 Feb 2022 16:47:29 +0000

WIP add docstrings to persist

Diffstat:
Mshep/persist.py | 71++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mshep/state.py | 319++++++++++++++++++++++++++++++++++++++++++-------------------------------------
2 files changed, 229 insertions(+), 161 deletions(-)

diff --git a/shep/persist.py b/shep/persist.py @@ -4,6 +4,15 @@ from .error import StateItemExists 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): super(PersistedState, self).__init__(bits, logger=logger) @@ -11,12 +20,17 @@ class PersistedState(State): self.__stores = {} + # Create state store container if missing. def __ensure_store(self, k): if self.__stores.get(k) == None: self.__stores[k] = self.__store_factory(k) 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) k = self.name(to_state) @@ -26,6 +40,10 @@ class PersistedState(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) k_from = self.name(from_state) @@ -41,6 +59,10 @@ class PersistedState(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) k_from = self.name(from_state) @@ -57,11 +79,16 @@ class PersistedState(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) to_state = super(PersistedState, self).move(key, 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): k_from = self.name(from_state) k_to = self.name(to_state) @@ -76,6 +103,13 @@ class PersistedState(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) self.__ensure_store(k) @@ -89,36 +123,51 @@ class PersistedState(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) self.__ensure_store(k) #return self.__stores[k].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): + """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) self.__ensure_store(k) return self.__stores[k].path(key=key) def next(self, key=None): + """Advance and persist to the next pure state. + + See shep.state.State.next + """ from_state = self.state(key) to_state = super(PersistedState, self).next(key) return self.__movestore(key, from_state, to_state) def replace(self, key, contents): + """Replace contents associated by content key. + + See shep.state.State.replace + """ super(PersistedState, self).replace(key, contents) state = self.state(key) k = self.name(state) diff --git a/shep/state.py b/shep/state.py @@ -118,10 +118,11 @@ class 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): + """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 try: idx = state_list.index(item) @@ -134,29 +135,31 @@ class State: 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): + """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 k = self.__check_name(k) v = self.__check_value(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): + """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) v = 0 for a in args: @@ -167,11 +170,12 @@ class State: self.__set(k, v) - # Return list of all unique atomic and alias states. - # - # :rtype: list of ints - # :return: states def all(self): + """Return list of all unique atomic and alias states. + + :rtype: list of ints + :return: states + """ l = [] for k in dir(self): if k[0] == '_': @@ -183,14 +187,15 @@ class State: 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): + """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: return 'NEW' k = self.__reverse.get(v) @@ -199,31 +204,33 @@ class State: 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): + """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) 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): + """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 if not pure: alias = self.__reverse.get(v) @@ -242,23 +249,24 @@ class State: 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): + """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: state = self.NEW elif self.__reverse.get(state) == None: @@ -271,17 +279,18 @@ class 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): + """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) if current_state == None: raise StateItemNotFound(key) @@ -311,18 +320,19 @@ class 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): + """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): raise ValueError('can only apply using single bit states') @@ -338,20 +348,21 @@ class 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): + """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): raise ValueError('can only apply using single bit states') @@ -373,70 +384,76 @@ class 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): + """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) if state == None: raise StateItemNotFound(key) 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): + """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) - # 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): + """List all content keys matching a state. + + :param state: State to match + :type state: int + :rtype: list of str + :returns: Matching content keys + """ try: return self.__keys[state] except KeyError: 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): + """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 - # 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): + """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 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): + """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) if state == None: raise StateItemNotFound(key) @@ -453,27 +470,29 @@ class 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): + """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) new_state = self.peek(key) 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): + """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.__contents[key] = contents