commit 39a46ca95d2b6c69ae5aecf8cd02906ca108238d
Author: lash <dev@holbrook.no>
Date: Sun, 4 Jan 2026 10:40:31 +0100
initial commit
Diffstat:
10 files changed, 376 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,3 @@
+build/
+*.egg-info
+__pycache__
diff --git a/readme.txt b/readme.txt
@@ -0,0 +1,4 @@
+No setup yet, extensions tested with following module versions:
+
+CouchDB==1.2
+valkey==6.1.1
diff --git a/tests/test_couchdb.py b/tests/test_couchdb.py
@@ -0,0 +1,20 @@
+import logging
+import unittest
+
+from whee.couchdb import CouchDBStore
+
+logging.basicConfig(level=logging.DEBUG)
+logg = logging.getLogger()
+
+class TestCouchDB(unittest.TestCase):
+
+ def setUp(self):
+ self.store = CouchDBStore('test', 'ya0JK6)hp')
+
+
+ def test_get_put(self):
+ pass
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_mem.py b/tests/test_mem.py
@@ -0,0 +1,35 @@
+import logging
+import unittest
+
+from whee.mem import MemStore
+
+logging.basicConfig(level=logging.DEBUG)
+logg = logging.getLogger()
+
+
+class TestMem(unittest.TestCase):
+
+ def setUp(self):
+ self.store = MemStore()
+
+
+ def test_get_put(self):
+ r = self.store.have(b'foo')
+ self.assertFalse(r)
+ self.store.put(b'foo', b'bar')
+ r = self.store.have(b'foo')
+ self.assertTrue(r)
+ r = self.store.get(b'foo')
+ self.assertEqual(r, b'bar')
+ r = self.store.get(b'foo'.hex())
+ self.assertEqual(r, b'bar')
+ with self.assertRaises(FileExistsError):
+ self.store.put(b'foo', b'baz')
+ self.store.put(b'foo', b'baz', exist_ok=True)
+ self.store.delete(b'foo')
+ with self.assertRaises(FileNotFoundError):
+ self.store.delete(b'foo')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_valkey.py b/tests/test_valkey.py
@@ -0,0 +1,38 @@
+import logging
+import unittest
+
+from whee.valkey import ValkeyStore
+
+logging.basicConfig(level=logging.DEBUG)
+logg = logging.getLogger()
+
+class TestValkey(unittest.TestCase):
+
+ def setUp(self):
+ self.store = ValkeyStore(124)
+
+ def test_get_put(self):
+ # remove the initial delete once we are creating temporary test dbs
+ try:
+ self.store.delete(b'foo')
+ except:
+ pass
+ r = self.store.have(b'foo')
+ self.assertFalse(r)
+ self.store.put(b'foo', b'bar')
+ r = self.store.have(b'foo')
+ self.assertTrue(r)
+ r = self.store.get(b'foo')
+ self.assertEqual(r, b'bar')
+ r = self.store.get(b'foo'.hex())
+ self.assertEqual(r, b'bar')
+ with self.assertRaises(FileExistsError):
+ self.store.put(b'foo', b'baz')
+ self.store.put(b'foo', b'baz', exist_ok=True)
+ self.store.delete(b'foo')
+ with self.assertRaises(FileNotFoundError):
+ self.store.delete(b'foo')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/whee/__init__.py b/whee/__init__.py
@@ -0,0 +1,29 @@
+from .base import Interface
+
+
+def ensure_hex_key(v):
+ """Return hex for bytes, or verify valid hex.
+
+ :param v: Value to check or convert.
+ :type v: bytes or str
+ :raises: ValueError if invalid hexadecimal value (and not bytes).
+ :returns: Corresponding hexadecimal data.
+ :rtype: str
+ """
+ if isinstance(v, bytes):
+ return v.hex()
+ bytes.fromhex(v)
+ return v
+
+
+def ensure_bytes_key(v):
+ """Return bytes key if hex is given.
+
+ :param v: Value to check or convert.
+ :type v: bytes or str
+ :returns: Corresponding bytes data
+ :rtype: bytes
+ """
+ if isinstance(v, bytes):
+ return v
+ return bytes.fromhex(v)
diff --git a/whee/base.py b/whee/base.py
@@ -0,0 +1,104 @@
+class Interface:
+ """This is Interface class abstracts transactional store operations on key-value-based backends.
+ """
+
+ def get(self, k):
+ """Retrieve value for key.
+
+ :raises: FileNotFoundError if key does not exist.
+ :raises: ConnectionRefusedError if store is locked.
+ :raises: IOError if key is found but read fails for any reason.
+ :returns: Value
+ :rtype: bytes
+ """
+ raise NotImplementedError()
+
+
+ def put(self, k, v, exist_ok=False):
+ """Retrieve value for key.
+
+ :raises: ValueError if value is in a format that cannot be stored.
+ :raises: ConnectionRefusedError if store is locked.
+ :raises: FileExistsError if key already exists and exist_ok is not True.
+ :raises: IOError if value is valid and key is available, but write fails for any other reason.
+ :returns: Value
+ :rtype: bytes
+ """
+ raise NotImplementedError()
+
+
+ def have(self, k):
+ """Check if key exists in store.
+
+ :raises: FileNotFoundError if key does not exist.
+ :raises: ConnectionRefusedError if store is locked.
+ :raises: IOError if value is valid and key is available, but write fails for any other reason.
+ """
+ raise NotImplementedError()
+
+
+ def start(self):
+ """Start a store transaction.
+
+ :raises: ConnectionError if the transaction cannot be made due to missing connection with the backend.
+ :raises: ConnectionRefusedError if store is locked.
+ :raises: PermissionError if a transaction is already in place, and/or the backend does not support (multiple) transactions.
+ :raises: IOError if lock cannot be placed for any other reason.
+ """
+ raise NotImplementedError()
+
+
+ def stop(self):
+ """Commit and end a store transaction.
+
+ :raises: ConnectionError if no transaction exists.
+ :raises: ConnectionRefusedError if store is locked.
+ :raises: ConnectionAbortedError if transaction could not be committed. After this, the transaction has been dropped.
+ :raises: IOError if transaction abort fails for any reason (transaction will still be pending).
+ """
+ raise NotImplementedError()
+
+
+ def delete(self, k):
+ """Delete key and its corresponding value. This action is not reversible.
+
+ :raises: FileNotFoundError if key does not exist.
+ :raises: ConnectionRefusedError if store is locked.
+ :raises: IOError if key is valid but operation fails for any other reason.
+ """
+ raise NotImplementedError()
+
+
+ def lock(self):
+ """Lock the store for any operation by any process.
+
+ :raises: ConnectionRefusedError if store is already locked.
+ :raises: IOError if lock fails for any other reason.
+ """
+ raise NotImplementedError()
+
+
+ def abort(self):
+ """Stop a store transaction without committing.
+
+ :raises: ConnectionError if no transaction exists.
+ :raises: ConnectionRefusedError if store is locked.
+ :raises: IOError if transaction abort fails for any reason (transaction will still be pending).
+ """
+ raise NotImplementedError()
+
+
+ def flush(self):
+ """Write changes to store and unlock.
+
+ :raises: IOError if write fails for any reason.
+ """
+ raise NotImplementedError()
+
+
+ def cap(self):
+ """Return bytes available for storage.
+
+ :raises: IOError if query fails for any reason.
+ """
+ return 0
diff --git a/whee/couchdb/__init__.py b/whee/couchdb/__init__.py
@@ -0,0 +1,33 @@
+import logging
+
+import couchdb
+
+from whee import Interface, ensure_hex_key
+
+logg = logging.getLogger('whee.couchdb')
+
+
+class CouchDBStore(Interface):
+ """Implements whee.Interface for Apache CouchDB
+ """
+
+ dbname_prefix = 'whee-'
+
+ def __init__(self, dbname, passphrase, user='admin', host='localhost', port=5984, ssl=False):
+ self.dbname = self.dbname_prefix + dbname
+ connstr = 'http'
+ if ssl:
+ connstr += 's'
+ connstr += '://'
+ connstr += '{}:{}@{}:{}/'.format(user, passphrase, host, port)
+ self.conn = couchdb.Server(connstr)
+ try:
+ self.db = self.conn.create(self.dbname)
+ except couchdb.http.PreconditionFailed:
+ self.db = self.conn[self.dbname]
+
+
+ def get(self, k):
+ #try:
+ # self.db.find('selector': {'type': 'wheekv'},
+ pass
diff --git a/whee/mem.py b/whee/mem.py
@@ -0,0 +1,55 @@
+import logging
+
+from whee import Interface, ensure_hex_key
+
+logg = logging.getLogger('memstore')
+
+
+class MemStore(Interface):
+ """Memstore implements the whee.Interface for python dicts in in-process memory.
+ """
+
+ def __init__(self):
+ self.v = {}
+ self.__to_store_key = ensure_hex_key
+
+
+ def have(self, k):
+ k = self.__to_store_key(k)
+ return bool(self.v.get(k))
+
+
+ def get(self, k):
+ k = self.__to_store_key(k)
+ r = self.v.get(k)
+ if r == None:
+ raise FileNotFoundError()
+ logg.debug('memstore get {} -> {}'.format(k, r))
+ return r
+
+
+ def put(self, k, v, exist_ok=False):
+ k = self.__to_store_key(k)
+ if self.have(k):
+ if not exist_ok:
+ raise FileExistsError()
+ logg.debug('memstore put (replace) {} <- {}'.format(k, v))
+ else:
+ logg.debug('memstore put {} <- {}'.format(k, v))
+ self.v[k] = v
+
+
+ def delete(self, k):
+ k = self.__to_store_key(k)
+ if not self.have(k):
+ raise FileNotFoundError
+ logg.debug('memstore delete {}'.format(k))
+ del self.v[k]
+
+
+ def start(self):
+ raise PermissionError()
+
+
+ def stop(self):
+ raise PermissionError()
diff --git a/whee/valkey/__init__.py b/whee/valkey/__init__.py
@@ -0,0 +1,55 @@
+import logging
+
+import valkey
+
+from whee import Interface, ensure_bytes_key
+
+logg = logging.getLogger('whee.valkey')
+
+
+class ValkeyStore(Interface):
+ """Implements whee.Interface for Valkey
+ """
+
+ dbno_default = 0
+
+ def __init__(self, passphrase, user=None, host='localhost', port=6379, dbno=None):
+ if dbno == None:
+ dbno = self.dbno_default
+ self.db = valkey.Valkey(host=host, port=port, db=dbno)
+ if user != None:
+ self.db.auth(user, passphrase)
+ self.db.ping()
+
+
+ def have(self, k):
+ k = ensure_bytes_key(k)
+ return bool(self.db.get(k))
+
+
+ def get(self, k):
+ k = ensure_bytes_key(k)
+ r = self.db.get(k)
+ if r == None:
+ raise FileNotFoundError()
+ logg.debug('valkeystore get {} -> {}'.format(k, r))
+ return r
+
+
+ def put(self, k, v, exist_ok=False):
+ k = ensure_bytes_key(k)
+ if self.have(k):
+ if not exist_ok:
+ raise FileExistsError()
+ logg.debug('valkeystore put (replace) {} <- {}'.format(k, v))
+ else:
+ logg.debug('valkeystore put {} <- {}'.format(k, v))
+ self.db.set(k, v)
+
+
+ def delete(self, k):
+ k = ensure_bytes_key(k)
+ if not self.have(k):
+ raise FileNotFoundError
+ logg.debug('valkeystore delete {}'.format(k))
+ self.db.delete(k)