commit 8c12346c5991f6cfacac33114506916efbf01ab1
parent 03061bbc706d5bc7c94660d444f108dec0931e58
Author: lash <dev@holbrook.no>
Date: Sun, 24 May 2026 16:31:59 -0600
Add partial match iterator to fs
Diffstat:
4 files changed, 117 insertions(+), 5 deletions(-)
diff --git a/setup.py b/setup.py
@@ -6,7 +6,7 @@ f.close()
setup(
name='wheepy',
- version='0.0.5a2',
+ version='0.0.6a1',
description='Unified interface to key value with locking and transactions.',
author='Louis Holbrook',
author_email='dev@holbrook.no',
diff --git a/tests/test_fs.py b/tests/test_fs.py
@@ -22,7 +22,7 @@ class TestFs(unittest.TestCase):
shutil.rmtree(self.base)
- def test_get_put(self):
+ def test_fs_get_put(self):
r = self.store.have(b'foo')
self.assertFalse(r)
self.store.put(b'foo', b'bar')
@@ -40,11 +40,37 @@ class TestFs(unittest.TestCase):
self.store.delete(b'foo')
- def test_short(self):
+ def test_fs_short(self):
self.store.put(b'f', b'oo')
self.store.put(b'fo', b'o')
self.store.put(b'foo', b'')
+ def test_fs_match(self):
+ self.store.put(b'afoo', b'inky')
+ self.store.put(b'abar', b'pinky')
+ self.store.put(b'abaz', b'blinky')
+ self.store.put(b'barbarbar', b'blinky')
+ self.store.match(b'a')
+ r = list(self.store)
+ self.assertEqual(len(r), 3)
+
+ self.store.match(b'ab')
+ r = list(self.store)
+ self.assertEqual(len(r), 2)
+
+
+ def test_fs_all(self):
+ self.store.put(b'afoo', b'inky')
+ self.store.put(b'abar', b'pinky')
+ self.store.put(b'abaz', b'blinky')
+ self.store.put(b'barbarbar', b'blinky')
+ r = list(self.store)
+ self.assertEqual(len(r), 4)
+
+ r = list(self.store.iter())
+ self.assertEqual(len(r), 4)
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/whee/base.py b/whee/base.py
@@ -37,6 +37,14 @@ class Interface:
raise NotImplementedError()
+ def match(self, k):
+ """Returns an iterator matching all stored keys that start with the given key.
+
+ :raises: IOError if query fails for any reason.
+ """
+ raise NotImplementedError()
+
+
def start(self):
"""Start a store transaction.
diff --git a/whee/fs/__init__.py b/whee/fs/__init__.py
@@ -17,6 +17,9 @@ class FsStore(Interface):
os.makedirs(self.base, exist_ok=True)
self.levels = levels
logg.info('create fsstore {}'.format(self.base))
+ self.k = None
+ self.i = -1
+ self.l = 0
def have(self, k):
@@ -46,20 +49,90 @@ class FsStore(Interface):
logg.debug('fs put {} under {}'.format(k, fp))
+ def match(self, k):
+ if self.l == 0:
+ self.k = []
+ self.i = 0
+ if isinstance(k, bytes):
+ k = k.hex()
+
+ if self.l == 0:
+ v = self.to_levelsub(k)
+ if v[0] == '_':
+ dp = os.path.join(self.base, '_')
+ d = []
+ try:
+ d = os.listdir(dp)
+ except FileNotFoundError:
+ pass
+ for v in d:
+ if v[0] == '.':
+ continue
+ if len(v) < len(k):
+ continue
+ if v[len(k)] == k:
+ self.k.append(k)
+
+ self.l += 1
+ dp = self.to_levelpath(k, trunc_short=False)
+ d = os.listdir(dp)
+ for v in d:
+ if v[0] == '.':
+ continue
+ if v[0] == '_':
+ continue
+ fp = os.path.join(dp, v)
+ part = k + v
+ if os.path.isdir(fp):
+ self.match(part)
+ continue
+ self.k.append(part)
+
+
+ def __iter__(self):
+ if self.k != None:
+ return self
+ self.match(b'')
+ return self
+
+
+ def iter(self):
+ self.k = None
+ self.i = -1
+ self.l = 0
+ return self.__iter__()
+
+
+ def __next__(self):
+ v = None
+ try:
+ v = self.k[self.i]
+ except IndexError:
+ raise StopIteration()
+ self.i += 1
+ return v
+
+
def delete(self, k):
fp = self.to_levelpath(k)
os.unlink(fp)
- def to_levelpath(self, k):
+ def to_levelsub(self, k, trunc_short=True):
s = ''
+ if len(k) == 0:
+ if trunc_short:
+ return self.base + '_'
+ else:
+ return self.base
+
if isinstance(k, bytes):
k = k.hex()
else:
bytes.fromhex(k)
l = len(k)
- if l <= self.levels * 2:
+ if l <= self.levels * 2 and trunc_short:
#levels = len(k) - 1
s = '_/' + k
else:
@@ -71,4 +144,9 @@ class FsStore(Interface):
s = s[:-1]
else:
s += k[c:]
+ return s
+
+
+ def to_levelpath(self, k, trunc_short=True):
+ s = self.to_levelsub(k, trunc_short=trunc_short)
return os.path.join(self.base, s)