commit 83dc671e5fecd68498414d6bd90fc01c82eb2726
parent 1504820466cd4b1a9d7c065b950d1ddd132451e6
Author: lash <dev@holbrook.no>
Date: Thu, 26 Mar 2026 12:55:13 -0600
Add fs store
Diffstat:
3 files changed, 108 insertions(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
@@ -6,7 +6,7 @@ f.close()
setup(
name='wheepy',
- version='0.0.3',
+ version='0.0.5',
description='Unified interface to key value with locking and transactions.',
author='Louis Holbrook',
author_email='dev@holbrook.no',
@@ -16,6 +16,7 @@ setup(
extras_require={
'couchdb': ['couchdb~=1.2'],
'valkey': ['valkey~=6.1.1'],
+ 'fs': ['xdg-base-dirs~=6.0.2'],
},
packages=[
'whee',
diff --git a/tests/test_fs.py b/tests/test_fs.py
@@ -0,0 +1,38 @@
+import logging
+import unittest
+import tempfile
+import uuid
+
+from whee.fs import FsStore
+
+logging.basicConfig(level=logging.DEBUG)
+logg = logging.getLogger()
+
+
+class TestFs(unittest.TestCase):
+
+ def setUp(self):
+ dp = tempfile.mkdtemp()
+ uu = uuid.uuid4()
+ self.store = FsStore(base=dp, dbname=str(uu), levels=2)
+
+
+ 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/whee/fs/__init__.py b/whee/fs/__init__.py
@@ -0,0 +1,68 @@
+import os
+import logging
+
+from xdg_base_dirs import xdg_data_home
+
+from whee import Interface
+
+logg = logging.getLogger('whee.fs')
+
+
+class FsStore(Interface):
+ """Implements whee.Interface for Fs backed store
+ """
+
+ def __init__(self, passphrase=None, base=xdg_data_home, dbname=None, levels=2):
+ self.base = os.path.join(base, dbname)
+ os.makedirs(self.base, exist_ok=True)
+ self.levels = levels
+ logg.info('create fsstore {}'.format(self.base))
+
+
+ def have(self, k):
+ fp = self.to_levelpath(k)
+ return os.path.isfile(fp)
+
+
+ def get(self, k):
+ fp = self.to_levelpath(k)
+ f = open(fp, 'rb')
+ v = f.read()
+ f.close()
+ logg.debug('fsstore get {} -> {}'.format(k, v))
+ return v
+
+
+ def put(self, k, v, exist_ok=False):
+ fp = self.to_levelpath(k)
+ if not exist_ok:
+ if os.path.exists(fp):
+ raise FileExistsError(k.hex())
+ dp = os.path.dirname(fp)
+ os.makedirs(dp, exist_ok=True)
+ f = open(fp, 'wb')
+ f.write(v)
+ f.close()
+
+
+ def delete(self, k):
+ fp = self.to_levelpath(k)
+ os.unlink(fp)
+
+
+ def to_levelpath(self, k):
+ if isinstance(k, bytes):
+ k = k.hex()
+ else:
+ bytes.fromhex(k)
+ s = ''
+ l = len(k)
+ c = 0
+ for i in range(self.levels):
+ s += k[c:c+2] + '/'
+ c += 2
+ if c == l:
+ s = s[:-1]
+ else:
+ s += k[c:]
+ return os.path.join(self.base, s)