shep

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

state.py (22494B)


      1 # standard imports
      2 import re
      3 import datetime
      4 import logging
      5 logg = logging.getLogger()
      6 
      7 # local imports
      8 from shep.error import (
      9         StateExists,
     10         StateInvalid,
     11         StateItemExists,
     12         StateItemNotFound,
     13         StateTransitionInvalid,
     14         StateCorruptionError,
     15         )
     16 
     17 re_name = r'^[a-zA-Z_\.]+$'
     18 
     19 def join_elements(states):
     20     return '_' + '__'.join(states)
     21 
     22 
     23 def split_elements(s):
     24     if len(s) == 0:
     25         return []
     26     if s[0] == '_':
     27         s = s[1:]
     28     return s.split('__')
     29 
     30 
     31 class State:
     32     """State is an in-memory bitmasked state store for key-value pairs, or even just keys alone.
     33 
     34     A State is comprised of a number of atomic state bits, and zero or more aliases that represent unique combinations of these bits.
     35 
     36     The State object will enforce that duplicate states cannot exist. It will also enforce that all alias states are composed of valid atomic states.
     37 
     38     :param bits: Number of atomic states that this State object will represent (i.e. number of bits).
     39     :type bits: int
     40     :param logger: Standard library logging instance to output to
     41     :type logger: logging.Logger
     42     """
     43 
     44     base_state_name = 'NEW'
     45 
     46     def __init__(self, bits, logger=None, verifier=None, check_alias=True, event_callback=None, default_state=None):
     47         self.__initial_bits = bits
     48         self.__bits = bits
     49         self.__limit = (1 << bits) - 1
     50         self.__c = 0
     51 
     52         self.__keys_reverse = {}
     53 
     54         if default_state == None:
     55             default_state = self.base_state_name
     56         else:
     57             default_state = self.__check_name_valid(default_state)
     58             self.base_state_name = default_state
     59             self.__keys_reverse[default_state] = 0
     60 
     61         setattr(self, default_state, 0)
     62 
     63         self.__reverse = {0: default_state}
     64         self.__keys = {0: []}
     65 
     66         self.__contents = {}
     67         self.modified_last = {}
     68         self.verifier = verifier
     69         self.check_alias = check_alias
     70         self.event_callback = event_callback
     71 
     72 
     73     @classmethod
     74     def set_default_state(cls, state_name):
     75         cls.base_state_name = state_name.upper()
     76 
     77 
     78     # return true if v is a single-bit state
     79     def is_pure(self, v):
     80         if v == 0:
     81             return True
     82         c = 1
     83         for i in range(self.__bits):
     84             if c & v > 0:
     85                 break
     86             c <<= 1
     87         return c == v
     88 
     89 
     90     # validates a state name and return its canonical representation
     91     def __check_name_valid(self, k):
     92         if not re.match(re_name, k):
     93             raise ValueError('only alpha and underscore')
     94         return k.upper()
     95 
     96 
     97     # enforces name validity, aswell as name uniqueness
     98     def __check_name(self, k):
     99         k = self.__check_name_valid(k) 
    100             
    101         try:
    102             getattr(self, k)
    103             raise StateExists(k)
    104         except AttributeError:
    105             pass
    106         return k
    107 
    108 
    109     # enforces state value validity and uniqueness
    110     def __check_valid(self, v):
    111         v = self.__check_value_typ(v)
    112         if self.__reverse.get(v):
    113             raise StateExists(v)
    114         return v
    115 
    116 
    117     # enforces state value within bit limit of instantiation
    118     def __check_limit(self, v, pure=True):
    119         if pure:
    120             if self.__initial_bits == 0:
    121                 self.__bits += 1
    122             self.__limit = (1 << self.__bits) - 1
    123         if v > self.__limit:
    124             raise OverflowError(v)
    125         return v
    126 
    127 
    128     # enforces state value validity, uniqueness and value limit 
    129     def __check_value(self, v):
    130         v = self.__check_valid(v)
    131         self.__check_limit(v) 
    132         return v
    133 
    134 
    135     # enforces state value validity
    136     def __check_value_typ(self, v):
    137         return int(v)
    138 
    139 
    140     # enforces state value validity within the currently registered states (number of add calls vs number of bits in instantiation).
    141     def __check_value_cursor(self, v):
    142         v = self.__check_value_typ(v)
    143         if v > 1 << self.__c:
    144             raise StateInvalid(v)
    145         return v
    146 
    147 
    148     # set a bit for state of the given key
    149     def __set(self, k, v):
    150         setattr(self, k, v)
    151         self.__reverse[v] = k
    152         self.__c += 1
    153 
    154 
    155     # check validity of key to register state for
    156     def __check_key(self, item):
    157         if self.__keys_reverse.get(item) != None:
    158             raise StateItemExists(item)
    159 
    160 
    161     # adds a new key to the state store
    162     def __add_state_list(self, state, item):
    163         if self.__keys.get(state) == None:
    164             self.__keys[state] = []
    165         if not self.is_pure(state) or state == 0:
    166             self.__keys[state].append(item)
    167         c = 1
    168         for i in range(self.__bits):
    169             part = c & state
    170             if part > 0:
    171                 if self.__keys.get(part) == None:
    172                     self.__keys[part] = []
    173                 self.__keys[part].append(item)
    174             c <<= 1
    175         self.__keys_reverse[item] = state
    176         if self.__reverse.get(state) == None and not self.check_alias:
    177             s = self.elements(state)
    178             self.__alias(s, state)
    179 
    180 
    181     def __state_list_index(self, item, state_list):
    182         """Get index of a key for a given state.
    183         A key should only ever exist in one state.
    184         A failed lookup should indicate a mistake on the caller part, (it may also indicate corruption, but probanbly impossible to tell the difference)
    185         """
    186         idx = -1
    187         try:
    188             idx = state_list.index(item)
    189         except ValueError:
    190             pass
    191 
    192         if idx == -1:
    193             raise StateCorruptionError() # should have state int here as value
    194 
    195         return idx
    196 
    197 
    198     def add(self, k):
    199         """Add a state to the store.
    200         
    201         :param k: State name
    202         :type k: str
    203         :raises shep.error.StateExists: State name is already registered
    204         """
    205         v = 1 << self.__c
    206         k = self.__check_name(k)
    207         v = self.__check_value(v)
    208         self.__set(k, v)
    209         return v
    210 
    211 
    212     def to_name(self, k):
    213         if k == None:
    214             k = 0
    215         return self.name(k)
    216 
    217 
    218     def __alias(self, k, *args):
    219         v = 0
    220         for a in args:
    221             a = self.__check_value_cursor(a)
    222             v = self.__check_limit(v | a, pure=False)
    223         if self.is_pure(v):
    224             raise ValueError('use add to add pure values')
    225         k = k.replace('.', '__')
    226         return self.__set(k, v)
    227 
    228 
    229     def alias(self, k, *args):
    230         """Add an alias for a combination of states in the store.
    231         
    232         State aggregates may be provided as comma separated values or as a single (or'd) integer value. 
    233         
    234         :param k: Alias name
    235         :type k: str
    236         :param *args: One or more states to aggregate for this alias.
    237         :type *args: int or list of ints
    238         :raises StateInvalid: Attempt to create alias for one or more atomic states that do not exist.
    239         :raises ValueError: Attempt to use bit value as alias
    240         """
    241         k = self.__check_name(k)
    242         return self.__alias(k, *args)    
    243 
    244 
    245     def __all_bit(self):
    246         r = []
    247         r.append(self.name(0))
    248         v = 1
    249         while v < self.__limit:
    250             r.append(self.name(v))
    251             v <<= 1
    252         return r
    253 
    254 
    255     def all(self, pure=False, numeric=False, ignore_auto=True, bit_order=False):
    256         """Return list of all unique atomic and alias state strings.
    257         
    258         :rtype: list of ints
    259         :return: states
    260         """
    261         l = []
    262         v = None
    263         if bit_order:
    264             v = self.__all_bit()
    265         else:
    266             v = dir(self)
    267         for k in v:
    268             state = None
    269             if k[0] == '_' and ignore_auto:
    270                 continue
    271             if k.upper() != k:
    272                 continue
    273             if pure:
    274                 state = self.from_name(k)
    275                 if not self.is_pure(state):
    276                     continue
    277             if numeric:
    278                 if state == None:
    279                     state = self.from_name(k)
    280                 l.append(state)
    281             else:
    282                 l.append(k)
    283         if not bit_order:
    284             l.sort()
    285         return l
    286 
    287 
    288     def elements(self, v, numeric=False, as_string=True):
    289         r = []
    290         if v == None or v == 0:
    291             return self.base_state_name
    292         c = 1
    293         for i in range(self.__bits):
    294             if (v & c) > 0:
    295                 if numeric:
    296                     r.append(c)
    297                 else:
    298                     r.append(self.name(c))
    299             c <<= 1
    300 
    301         if numeric or not as_string:
    302             return r
    303 
    304         if len(r) == 1:
    305             return self.name(v)
    306 
    307         return join_elements(r) #'_' + '.'.join(r)
    308 
    309 
    310     def from_elements(self, k, create_missing=False):
    311         r = 0
    312         if k[0] != '_':
    313             raise ValueError('elements string must start with underscore (_), got {}'.format(k))
    314         for v in k[1:].split('__'):
    315             state = None
    316             try:
    317                 state = self.from_name(v) 
    318             except AttributeError as e:
    319                 pass
    320 
    321             if state == None:
    322                 if not create_missing: 
    323                     raise StateInvalid(v)
    324                 state = self.add(v)
    325 
    326             r |= state
    327         return r
    328 
    329 
    330     def name(self, v):
    331         """Retrieve that string representation of the state attribute represented by the given state integer value.
    332         
    333         :param v: State integer
    334         :type v: int
    335         :raises StateInvalid: State corresponding to given integer not found
    336         :rtype: str
    337         :return: State name
    338         """
    339         k = self.__reverse.get(v)
    340         if k == None:
    341             if self.check_alias:
    342                 raise StateInvalid(v)
    343             else:
    344                 k = self.elements(v)
    345         elif v == None or v == 0:
    346             return self.base_state_name
    347         return k
    348 
    349 
    350     def from_name(self, k):
    351         """Retrieve the real state integer value corresponding to an attribute name.
    352         
    353         :param k: Attribute name
    354         :type k: str
    355         :raises ValueError: Invalid attribute name
    356         :raises AttributeError: Attribute not found
    357         :rtype: int
    358         :return: Numeric state value
    359         """
    360         k = self.__check_name_valid(k)
    361         if k == self.base_state_name:
    362             return 0
    363         return getattr(self, k)
    364 
    365 
    366     def match(self, v, pure=False):
    367         """Match against all stored states.
    368         
    369         If pure is set, only match against the single atomic state will be returned.
    370         
    371         :param v: Integer state to match
    372         :type v: int
    373         :param pure: Match only pure states
    374         :type pure: bool
    375         :raises KeyError: Unknown state
    376         :rtype: tuple
    377         :return: 0: Alias that input resolves to, 1: list of atomic states that matches the state
    378         """
    379         alias = None
    380         if not pure:
    381             alias = self.__reverse.get(v)
    382 
    383         r = []
    384         c = 1
    385         for i in range(self.__bits):
    386             if v & c > 0:
    387                 try:
    388                     k = self.__reverse[c]
    389                     r.append(k)
    390                 except KeyError:
    391                     pass
    392             c <<= 1
    393 
    394         return (alias, r,)
    395 
    396    
    397     def put(self, key, state=None, contents=None):
    398         """Add a key to an existing state.
    399         
    400         If no state it specified, the default state attribute State.base_state_name will be used.
    401         
    402         Contents may be supplied as value to pair with the given key. Contents may be changed later by calling the `replace` method.
    403         
    404         :param key: Content key to add
    405         :type key: str
    406         :param state: Initial state for the put. If not given, initial state will be State.base_state_name
    407         :type state: int
    408         :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
    409         :type contents: str
    410         :raises StateItemExists: Content key has already been added
    411         :raises StateInvalid: Given state has not been registered
    412         :rtype: integer
    413         :return: Resulting state that key is put under (should match the input state)
    414         """
    415         if state == None:
    416             state = getattr(self, self.base_state_name)
    417         elif self.__reverse.get(state) == None and self.check_alias:
    418             raise StateInvalid(state)
    419         self.__check_key(key)
    420 
    421         if self.event_callback != None:
    422             old_state = self.__keys_reverse.get(key)
    423             self.event_callback(key, None, self.name(state))
    424 
    425         self.__add_state_list(state, key)
    426         if contents != None:
    427             self.__contents[key] = contents
    428 
    429         self.register_modify(key)
    430         return state
    431                                 
    432 
    433     def move(self, key, to_state):
    434         """Move a given content key from one state to another.
    435         
    436         :param key: Key to move
    437         :type key: str
    438         :param to_state: Numeric state to move to (may be atomic or alias)
    439         :type to_state: integer
    440         :raises StateItemNotFound: Given key has not been registered
    441         :raises StateInvalid: Given state has not been registered
    442         :rtype: integer
    443         :return: Resulting state from move (should match the state given as input)
    444         """
    445         current_state = self.__keys_reverse.get(key)
    446         if current_state == None:
    447             raise StateItemNotFound(key)
    448 
    449         new_state = self.__reverse.get(to_state)
    450         if new_state == None and self.check_alias:
    451             raise StateInvalid(to_state)
    452 
    453         return self.__move(key, current_state, to_state)
    454 
    455 
    456     # implementation for state move that ensures integrity of keys and states.
    457     def __move(self, key, from_state, to_state):
    458         current_state_list = self.__keys.get(from_state)
    459         if current_state_list == None:
    460             raise StateCorruptionError(current_state)
    461 
    462         idx = self.__state_list_index(key, current_state_list)
    463 
    464         new_state_list = self.__keys.get(to_state)
    465         if current_state_list == None:
    466             raise StateCorruptionError(to_state)
    467 
    468         if self.verifier != None:
    469             r = self.verifier(self, key, from_state, to_state)
    470             if r != None:
    471                 raise StateTransitionInvalid(r)
    472 
    473         old_state = self.__keys_reverse.get(key)
    474         if self.event_callback != None:
    475             self.event_callback(key, self.name(old_state), self.name(to_state))
    476 
    477         if old_state == 0:
    478             current_state_list.pop(idx)
    479         else:
    480             for k in self.elements(from_state, numeric=True):
    481                 self.__keys[k].remove(key)
    482         self.__add_state_list(to_state, key)
    483 
    484         self.register_modify(key)
    485 
    486         logg.debug('move {} {} {}'.format(key, from_state, to_state))
    487         return to_state
    488    
    489 
    490     def set(self, key, or_state):
    491         """Move to an alias state by setting a single bit.
    492         
    493         :param key: Content key to modify state for
    494         :type key: str
    495         :param or_state: Atomic stat to add
    496         :type or_state: int
    497         :raises ValueError: State is not a single bit state
    498         :raises StateItemNotFound: Content key is not registered
    499         :raises StateInvalid: Resulting state after addition of atomic state is unknown
    500         :rtype: int
    501         :returns: Resulting state
    502         """
    503         if not self.is_pure(or_state):
    504             raise ValueError('can only apply using single bit states')
    505 
    506         current_state = self.__keys_reverse.get(key)
    507         if current_state == None:
    508             raise StateItemNotFound(key)
    509 
    510         to_state = current_state | or_state
    511         new_state = self.__reverse.get(to_state)
    512         if new_state == None and self.check_alias:
    513             raise StateInvalid('resulting to state is unknown: {}'.format(to_state))
    514         
    515         return self.__move(key, current_state, to_state)
    516 
    517 
    518     def unset(self, key, not_state, allow_base=False):
    519         """Unset a single bit, moving to a pure or alias state.
    520         
    521         If allow_base is set to False, The resulting state cannot be State.base_state_name (0).
    522          
    523         :param key: Content key to modify state for
    524         :type key: str
    525         :param or_state: Atomic stat to add
    526         :type or_state: int
    527         :paran allow_base: Allow state to be reset to 0
    528         :type allow_base: bool
    529         :raises ValueError: State is not a single bit state, or attempts to revert to State.base_state_name
    530         :raises StateItemNotFound: Content key is not registered
    531         :raises StateInvalid: Resulting state after addition of atomic state is unknown
    532         :rtype: int
    533         :returns: Resulting state
    534         """
    535         if not self.is_pure(not_state):
    536             raise ValueError('can only apply using single bit states')
    537 
    538         current_state = self.__keys_reverse.get(key)
    539         if current_state == None:
    540             raise StateItemNotFound(key)
    541 
    542         to_state = current_state & (~not_state)
    543         if to_state == current_state:
    544             raise ValueError('invalid change for state {}: {}'.format(key, not_state))
    545 
    546         if to_state == getattr(self, self.base_state_name) and not allow_base:
    547             raise ValueError('State {} for {} cannot be reverted to {}'.format(current_state, key, self.base_state_name))
    548 
    549         new_state = self.__reverse.get(to_state)
    550         if new_state == None:
    551             raise StateInvalid('resulting to state is unknown: {}'.format(to_state))
    552 
    553         return self.__move(key, current_state, to_state)
    554 
    555 
    556     def change(self, key, sets, unsets):
    557         current_state = self.__keys_reverse.get(key)
    558         if current_state == None:
    559             raise StateItemNotFound(key)
    560         to_state = current_state | sets
    561         to_state &= ~unsets & self.__limit
    562 
    563         if sets == 0:
    564             to_state = current_state & (~unsets)
    565             if to_state == current_state:
    566                 raise ValueError('invalid change by unsets for state {}: {}'.format(key, unsets))
    567 
    568         if to_state == getattr(self, self.base_state_name):
    569             raise ValueError('State {} for {} cannot be reverted to {}'.format(current_state, key, self.base_state_name))
    570 
    571         new_state = self.__reverse.get(to_state)
    572         if new_state == None:
    573             raise StateInvalid('resulting to state is unknown: {}'.format(to_state))
    574 
    575         return self.__move(key, current_state, to_state)
    576 
    577 
    578     def state(self, key):
    579         """Return the current numeric state for the given content key.
    580         
    581         :param key: Key to return content for
    582         :type key: str
    583         :raises StateItemNotFound: Content key is unknown
    584         :rtype: int
    585         :returns: State
    586         """
    587         state = self.__keys_reverse.get(key)
    588         if state == None:
    589             raise StateItemNotFound(key)
    590         return state
    591 
    592 
    593     def get(self, key):
    594         """Retrieve the content for a content key.
    595         
    596         :param key: Content key to retrieve content for
    597         :type key: str
    598         :rtype: any
    599         :returns: Content
    600         """
    601         return self.__contents.get(key)
    602 
    603 
    604     def list(self, state):
    605         """List all content keys matching a state.
    606         
    607         :param state: State to match 
    608         :type state: int
    609         :rtype: list of str
    610         :returns: Matching content keys
    611         """
    612         try:
    613             return self.__keys[state]
    614         except KeyError:
    615             return []
    616 
    617 
    618     def sync(self, state=None, not_state=None, ignore_auto=True):
    619         """Noop method for interface implementation providing sync to backend.
    620         
    621         :param state: State to sync.
    622         :type state:
    623         :todo: (for higher level implementer) if sync state is none, sync all
    624         """
    625         pass
    626 
    627 
    628     def path(self, state, key=None):
    629         """In the memory-only class no persisted state is used, and this will return None.
    630         
    631         See shep.persist.PersistedState.path for more information.
    632         """
    633         return None
    634 
    635 
    636     def peek(self, key):
    637         """Return the next pure state.
    638          
    639         Will return the same result as the method next, but without advancing to the new state.
    640         
    641         :param key: Content key to inspect state for
    642         :type key: str
    643         :raises StateItemNotFound: Unknown content key
    644         :raises StateInvalid: Attempt to advance from an alias state, OR beyond the last known pure state.
    645         :rtype: int
    646         :returns: Next state
    647         """
    648         state = self.__keys_reverse.get(key)
    649         if state == None:
    650             raise StateItemNotFound(key)
    651         if not self.is_pure(state):
    652             raise StateInvalid('cannot run next on an alias state')
    653        
    654         if state == 0:
    655             state = 1
    656         else:
    657             state <<= 1
    658         if state > self.__limit:
    659             raise StateInvalid('unknown state {}'.format(state))
    660 
    661         return state
    662 
    663 
    664     def next(self, key):
    665         """Advance to the next pure state.
    666            
    667         :param key: Content key to inspect state for
    668         :type key: str
    669         :raises StateItemNotFound: Unknown content key
    670         :raises StateInvalid: Attempt to advance from an alias state, OR beyond the last known pure state.
    671         :rtype: int
    672         :returns: Next state
    673         """
    674         from_state = self.state(key)
    675         new_state = self.peek(key)
    676         return self.__move(key, from_state, new_state)
    677 
    678 
    679     def replace(self, key, contents):
    680         """Replace contents associated by content key.
    681          
    682         :param key: Content key to replace for
    683         :type key: str
    684         :param contents: New contents
    685         :type contents: any
    686         :raises KeyError: Unknown content key
    687         """
    688         self.state(key)
    689         self.__contents[key] = contents
    690 
    691 
    692     def modified(self, key):
    693         return self.modified_last[key]
    694 
    695 
    696     def register_modify(self, key):
    697         self.modified_last[key] = datetime.datetime.utcnow().timestamp()
    698 
    699 
    700     def mask(self, key, states=0):
    701         statemask = self.__limit + 1
    702         statemask |= states
    703         statemask = ~statemask
    704         statemask &= self.__limit
    705         return statemask
    706 
    707 
    708     def purge(self, key):
    709         state = self.state(key)
    710         state_name = self.name(state)
    711 
    712         v = self.__keys.get(state)
    713         v.remove(key) 
    714 
    715         del self.__keys_reverse[key]
    716 
    717         try:
    718             del self.__contents[key]
    719         except KeyError:
    720             pass
    721 
    722         try:
    723             del self.modified_last[key]
    724         except KeyError:
    725             pass
    726 
    727 
    728     def count(self):
    729         return self.__c