Add redis store backend with tests
This commit is contained in:
parent
4fc8358e27
commit
2f7508ad6e
@ -1,3 +1,6 @@
|
||||
- 0.2.0
|
||||
* Add redis backend
|
||||
* UTC timestamp for modification time in core state
|
||||
- 0.1.1
|
||||
* Optional, pluggable verifier to protect state transition
|
||||
* Change method for atomic simultaneous set and unset
|
||||
|
@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = shep
|
||||
version = 0.1.1
|
||||
version = 0.2.0rc1
|
||||
description = Multi-state key stores using bit masks
|
||||
author = Louis Holbrook
|
||||
author_email = dev@holbrook.no
|
||||
|
@ -41,6 +41,8 @@ class PersistedState(State):
|
||||
self.__ensure_store(k)
|
||||
self.__stores[k].add(key, contents)
|
||||
|
||||
self.register_modify(key)
|
||||
|
||||
|
||||
def set(self, key, or_state):
|
||||
"""Persist a new state for a key or key/content.
|
||||
|
@ -613,7 +613,7 @@ class State:
|
||||
|
||||
|
||||
def register_modify(self, key):
|
||||
self.modified_last[key] = datetime.datetime.now().timestamp()
|
||||
self.modified_last[key] = datetime.datetime.utcnow().timestamp()
|
||||
|
||||
|
||||
def mask(self, key, states=0):
|
||||
|
94
shep/store/redis.py
Normal file
94
shep/store/redis.py
Normal file
@ -0,0 +1,94 @@
|
||||
# external imports
|
||||
import redis
|
||||
|
||||
|
||||
class RedisStore:
|
||||
|
||||
def __init__(self, path, redis, binary=False):
|
||||
self.redis = redis
|
||||
self.__path = path
|
||||
self.__binary = binary
|
||||
|
||||
def __to_path(self, k):
|
||||
return '.'.join([self.__path, k])
|
||||
|
||||
|
||||
def __from_path(self, s):
|
||||
(left, right) = s.split('.', maxsplit=1)
|
||||
return right
|
||||
|
||||
|
||||
def __to_result(self, v):
|
||||
if self.__binary:
|
||||
return v
|
||||
return v.decode('utf-8')
|
||||
|
||||
|
||||
def add(self, k, contents=b''):
|
||||
if contents == None:
|
||||
contents = b''
|
||||
k = self.__to_path(k)
|
||||
self.redis.set(k, contents)
|
||||
|
||||
|
||||
def remove(self, k):
|
||||
k = self.__to_path(k)
|
||||
self.redis.delete(k)
|
||||
|
||||
|
||||
def get(self, k):
|
||||
k = self.__to_path(k)
|
||||
v = self.redis.get(k)
|
||||
return self.__to_result(v)
|
||||
|
||||
|
||||
def list(self):
|
||||
(cursor, matches) = self.redis.scan(match=self.__path + '.*')
|
||||
|
||||
r = []
|
||||
for s in matches:
|
||||
k = self.__from_path(s)
|
||||
v = self.redis.get(v)
|
||||
r.append((k, v,))
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def path(self):
|
||||
return None
|
||||
|
||||
|
||||
def replace(self, k, contents):
|
||||
if contents == None:
|
||||
contents = b''
|
||||
k = self.__to_path(k)
|
||||
v = self.redis.get(k)
|
||||
if v == None:
|
||||
raise FileNotFoundError(k)
|
||||
self.redis.set(k, contents)
|
||||
|
||||
|
||||
def modified(self, k):
|
||||
k = self.__to_path(k)
|
||||
k = '_mod' + k
|
||||
v = self.redis.get(k)
|
||||
return int(v)
|
||||
|
||||
|
||||
def register_modify(self, k):
|
||||
k = self.__to_path(k)
|
||||
k = '_mod' + k
|
||||
ts = datetime.datetime.utcnow().timestamp()
|
||||
self.redis.set(k)
|
||||
|
||||
|
||||
class RedisStoreFactory:
|
||||
|
||||
def __init__(self, host='localhost', port=6379, db=0, binary=False):
|
||||
self.redis = redis.Redis(host=host, port=port, db=db)
|
||||
self.__binary = binary
|
||||
|
||||
|
||||
def add(self, k):
|
||||
k = str(k)
|
||||
return RedisStore(k, self.redis, binary=self.__binary)
|
@ -13,12 +13,12 @@ from shep.error import (
|
||||
)
|
||||
|
||||
|
||||
class TestStateReport(unittest.TestCase):
|
||||
class TestFileStore(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.d = tempfile.mkdtemp()
|
||||
self.factory = SimpleFileStoreFactory(self.d)
|
||||
self.states = PersistedState(self.factory.add, 4)
|
||||
self.states = PersistedState(self.factory.add, 3)
|
||||
self.states.add('foo')
|
||||
self.states.add('bar')
|
||||
self.states.add('baz')
|
||||
@ -206,6 +206,9 @@ class TestStateReport(unittest.TestCase):
|
||||
with self.assertRaises(StateInvalid):
|
||||
self.states.next('abcd')
|
||||
|
||||
v = self.states.state('abcd')
|
||||
self.assertEqual(v, self.states.BAZ)
|
||||
|
||||
fp = os.path.join(self.d, 'FOO', 'abcd')
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
os.stat(fp)
|
||||
|
93
tests/test_redis.py
Normal file
93
tests/test_redis.py
Normal file
@ -0,0 +1,93 @@
|
||||
# standard imports
|
||||
import unittest
|
||||
import os
|
||||
import logging
|
||||
import sys
|
||||
import importlib
|
||||
|
||||
# local imports
|
||||
from shep.persist import PersistedState
|
||||
from shep.error import (
|
||||
StateExists,
|
||||
StateInvalid,
|
||||
StateItemExists,
|
||||
StateItemNotFound,
|
||||
)
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logg = logging.getLogger()
|
||||
|
||||
|
||||
class TestRedisStore(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
from shep.store.redis import RedisStoreFactory
|
||||
self.factory = RedisStoreFactory()
|
||||
self.states = PersistedState(self.factory.add, 3)
|
||||
self.states.add('foo')
|
||||
self.states.add('bar')
|
||||
self.states.add('baz')
|
||||
|
||||
|
||||
def test_add(self):
|
||||
self.states.put('abcd', state=self.states.FOO, contents='baz')
|
||||
v = self.states.get('abcd')
|
||||
self.assertEqual(v, 'baz')
|
||||
v = self.states.state('abcd')
|
||||
self.assertEqual(v, self.states.FOO)
|
||||
|
||||
|
||||
def test_next(self):
|
||||
self.states.put('abcd')
|
||||
|
||||
self.states.next('abcd')
|
||||
self.assertEqual(self.states.state('abcd'), self.states.FOO)
|
||||
|
||||
self.states.next('abcd')
|
||||
self.assertEqual(self.states.state('abcd'), self.states.BAR)
|
||||
|
||||
self.states.next('abcd')
|
||||
self.assertEqual(self.states.state('abcd'), self.states.BAZ)
|
||||
|
||||
with self.assertRaises(StateInvalid):
|
||||
self.states.next('abcd')
|
||||
|
||||
v = self.states.state('abcd')
|
||||
self.assertEqual(v, self.states.BAZ)
|
||||
|
||||
|
||||
def test_replace(self):
|
||||
with self.assertRaises(StateItemNotFound):
|
||||
self.states.replace('abcd', contents='foo')
|
||||
|
||||
self.states.put('abcd', state=self.states.FOO, contents='baz')
|
||||
self.states.replace('abcd', contents='bar')
|
||||
v = self.states.get('abcd')
|
||||
self.assertEqual(v, 'bar')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
noredis = False
|
||||
redis = None
|
||||
try:
|
||||
redis = importlib.import_module('redis')
|
||||
except ModuleNotFoundError:
|
||||
logg.critical('redis module not available, skipping tests.')
|
||||
sys.exit(0)
|
||||
|
||||
host = os.environ.get('REDIS_HOST', 'localhost')
|
||||
port = os.environ.get('REDIS_PORT', 6379)
|
||||
port = int(port)
|
||||
db = os.environ.get('REDIS_DB', 0)
|
||||
db = int(db)
|
||||
r = redis.Redis(host=host, port=port, db=db)
|
||||
try:
|
||||
r.get('foo')
|
||||
except redis.exceptions.ConnectionError:
|
||||
logg.critical('could not connect to redis, skipping tests.')
|
||||
sys.exit(0)
|
||||
except redis.exceptions.InvalidResponse as e:
|
||||
logg.critical('is that really redis running on {}:{}? Got unexpected response: {}'.format(host, port, e))
|
||||
sys.exit(0)
|
||||
|
||||
unittest.main()
|
31
tests/test_verify.py
Normal file
31
tests/test_verify.py
Normal file
@ -0,0 +1,31 @@
|
||||
# standard imports
|
||||
import unittest
|
||||
|
||||
# local imports
|
||||
from shep import State
|
||||
from shep.error import (
|
||||
StateTransitionInvalid,
|
||||
)
|
||||
|
||||
|
||||
def mock_verify(state, from_state, to_state):
|
||||
if from_state == state.FOO:
|
||||
if to_state == state.BAR:
|
||||
return 'bar cannot follow foo'
|
||||
|
||||
|
||||
class TestState(unittest.TestCase):
|
||||
|
||||
def test_verify(self):
|
||||
states = State(2, verifier=mock_verify)
|
||||
states.add('foo')
|
||||
states.add('bar')
|
||||
states.put('xyzzy')
|
||||
states.next('xyzzy')
|
||||
with self.assertRaises(StateTransitionInvalid):
|
||||
states.next('xyzzy')
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
Reference in New Issue
Block a user