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