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
|
- 0.1.1
|
||||||
* Optional, pluggable verifier to protect state transition
|
* Optional, pluggable verifier to protect state transition
|
||||||
* Change method for atomic simultaneous set and unset
|
* Change method for atomic simultaneous set and unset
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = shep
|
name = shep
|
||||||
version = 0.1.1
|
version = 0.2.0rc1
|
||||||
description = Multi-state key stores using bit masks
|
description = Multi-state key stores using bit masks
|
||||||
author = Louis Holbrook
|
author = Louis Holbrook
|
||||||
author_email = dev@holbrook.no
|
author_email = dev@holbrook.no
|
||||||
|
@ -41,6 +41,8 @@ class PersistedState(State):
|
|||||||
self.__ensure_store(k)
|
self.__ensure_store(k)
|
||||||
self.__stores[k].add(key, contents)
|
self.__stores[k].add(key, contents)
|
||||||
|
|
||||||
|
self.register_modify(key)
|
||||||
|
|
||||||
|
|
||||||
def set(self, key, or_state):
|
def set(self, key, or_state):
|
||||||
"""Persist a new state for a key or key/content.
|
"""Persist a new state for a key or key/content.
|
||||||
|
@ -613,7 +613,7 @@ class State:
|
|||||||
|
|
||||||
|
|
||||||
def register_modify(self, key):
|
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):
|
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):
|
def setUp(self):
|
||||||
self.d = tempfile.mkdtemp()
|
self.d = tempfile.mkdtemp()
|
||||||
self.factory = SimpleFileStoreFactory(self.d)
|
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('foo')
|
||||||
self.states.add('bar')
|
self.states.add('bar')
|
||||||
self.states.add('baz')
|
self.states.add('baz')
|
||||||
@ -206,6 +206,9 @@ class TestStateReport(unittest.TestCase):
|
|||||||
with self.assertRaises(StateInvalid):
|
with self.assertRaises(StateInvalid):
|
||||||
self.states.next('abcd')
|
self.states.next('abcd')
|
||||||
|
|
||||||
|
v = self.states.state('abcd')
|
||||||
|
self.assertEqual(v, self.states.BAZ)
|
||||||
|
|
||||||
fp = os.path.join(self.d, 'FOO', 'abcd')
|
fp = os.path.join(self.d, 'FOO', 'abcd')
|
||||||
with self.assertRaises(FileNotFoundError):
|
with self.assertRaises(FileNotFoundError):
|
||||||
os.stat(fp)
|
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