commit c20eceaa8616fabcc5c8342f4d25192a51f76ec2
parent 12f64636de8e39b2755b5eae2ee70ec90c10d1a1
Author: lash <dev@holbrook.no>
Date: Sun, 22 Mar 2026 18:31:18 -0600
Initial functionality of iterative entry craft cli tool
Diffstat:
8 files changed, 217 insertions(+), 31 deletions(-)
diff --git a/dummy/tests/store.py b/dummy/tests/store.py
@@ -170,7 +170,7 @@ class TestStore(unittest.TestCase):
o.add_part(dst)
o.add_tag('foo')
o.add_pair('bar', 'baz')
- store.add_draft(o)
+ store.put_draft(o)
o = store.get_draft(o)
diff --git a/dummy/usawa/account.py b/dummy/usawa/account.py
@@ -10,7 +10,7 @@ def check_path_parts(path):
parts = path.split('/')
for v in parts:
if not v.isalnum():
- raise AccountError('invalid part: ' + v)
+ raise AccountError('invalid part: {} ({})'.format(v, v.encode('utf-8').hex()))
#return True
typ = getattr(AccountType, parts[0].lower())
return (typ, parts[1:],)
@@ -81,6 +81,28 @@ class AccountIndex:
self.iterfilter = None
+# @staticmethod
+# def from_io(self, io, closer=None):
+# while True:
+# v = io.readline()
+# if closer != None:
+# closer()
+#
+
+ @staticmethod
+ def from_file(unitindex, filepath):
+ o = AccountIndex(unitindex)
+ f = open(filepath, "r")
+ while True:
+ v = f.readline()
+ if not v:
+ break
+ o.add(v.strip())
+ f.close()
+# return AccountIndex.from_io(f, closer=f.close)
+ return o
+
+
def add(self, path, sym=None, typ=None):
account = Account.from_path(path, sym=sym, typ=typ)
try:
diff --git a/dummy/usawa/base.py b/dummy/usawa/base.py
@@ -0,0 +1,84 @@
+import uuid
+import logging
+
+import rencode
+
+
+logg = logging.getLogger('base')
+
+
+def sanitize_key(k):
+ if isinstance(k, bytes):
+ k = k.decode('utf-8')
+ elif not isinstance(k, str):
+ raise ValueError('key must be str')
+ if '=' in k:
+ raise ValueError('invalid key')
+ return k
+
+
+class UsawaElement:
+
+ def __init__(self, ref=None):
+ if ref == None:
+ self.ref = str(uuid.uuid4())
+ else:
+ self.ref = str(uuid.UUID(ref))
+ self.kv = {}
+
+
+ def get_ref(self, binary=False):
+ ref = self.ref
+ if binary:
+ ref = uuid.UUID(ref).bytes
+ return ref
+
+
+ def add_tag(self, k):
+ k = sanitize_key(k)
+ if self.kv.get(k):
+ raise KeyError("key '{}' exists".format(k))
+ self.kv[k] = True
+ logg.debug('add tag {} to {}'.format(k, self.ref))
+
+
+ def add_pair(self, k, v):
+ k = sanitize_key(k)
+ if self.kv.get(k):
+ raise KeyError('{} exists'.format(k))
+ self.kv[k] = v
+ logg.debug('add key {} to {}'.format(k, self.ref))
+
+
+ def get(self, k):
+ return self.kv.get(k)
+
+
+ def serialize(self):
+ d = []
+ for k in self.kv.keys():
+ d.append((k, self.kv[k],))
+ return d
+
+ b = self.to_list()
+ return rencode.dumps(b)
+
+
+ def deserialize(self, data):
+ try:
+ o = rencode.loads(data)
+ except TypeError:
+ o = data
+ self.t = []
+ self.kv = {}
+ i = 0
+ for i in range(len(o)):
+ v = o[i][1]
+ k = o[i][0]
+ k = sanitize_key(k)
+ if isinstance(v, bool):
+ self.add_tag(k)
+ continue
+ if isinstance(v, bytes):
+ v = v.decode('utf-8')
+ self.add_pair(k, v)
diff --git a/dummy/usawa/data/usawa.ini b/dummy/usawa/data/usawa.ini
@@ -1,17 +1,21 @@
[main]
gpg_dir =
+accounts_file =
+legder_file =
[valkey]
+id = 2434
host = localhost
port = 6379
[server]
socket_file_path=
-
[wallet]
key_passphrase =
[fs_resolver]
store_path=
+[store]
+type = valkey
diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py
@@ -206,6 +206,12 @@ class Entry(UsawaElement):
self.lookup_algo = None
+ @staticmethod
+ def empty():
+ dt = datetime.datetime.utcnow()
+ return Entry(-1, dt)
+
+
"""Add an entry part to the entry.
At least one debit and one credit item must be added to be valid.
diff --git a/dummy/usawa/runnable/craft.py b/dummy/usawa/runnable/craft.py
@@ -0,0 +1,65 @@
+import argparse
+import logging
+
+from whee.valkey import ValkeyStore
+
+import usawa.config
+from usawa import Entry, Ledger
+from usawa.store import EntryStore
+from usawa.account import AccountIndex
+
+logging.basicConfig(level=logging.WARNING)
+logg = logging.getLogger()
+
+
+class Context:
+
+ def __init__(self, args):
+ self.cfg = usawa.config.load_config(config_dir=args.c)
+ self.cmd = args.cmd
+
+ # set up ledger
+ s = args.l
+ if not s:
+ try:
+ s = self.cfg.get('MAIN_LEDGER_FILE')
+ except KeyError:
+ pass
+ if not s:
+ raise ValueError('ledger file required')
+ self.ledger = Ledger.from_file(s)
+
+ # set up accounts hierarchy, if applicable
+ self.accounts = None
+ s = self.cfg.get('MAIN_ACCOUNTS_FILE')
+ if s:
+ self.accounts = AccountIndex.from_file(self.ledger.uidx, s)
+ else:
+ self.accounts = AccountIndex(self.ledger.uidx)
+
+ self.entry = Entry.empty()
+ self.db = None
+ if self.cfg.get('STORE_TYPE') == 'valkey':
+ dbid = self.cfg.get('VALKEY_ID')
+ host = self.cfg.get('VALKEY_HOST')
+ port = self.cfg.get('VALKEY_PORT')
+ self.db = ValkeyStore('', host=host, port=port)
+ self.store = EntryStore(self.db)
+ if args.r:
+ self.entry = self.store.get_draft(self.entry)
+ else:
+ self.store.put_draft(self.entry)
+
+
+argp = argparse.ArgumentParser()
+argp.add_argument('-r', type=str, help='entry unique reference')
+argp.add_argument('-v', type=str, choices=['info','debug','warning','error'], help='be verbose')
+argp.add_argument('-c', type=str, help='override config dir')
+argp.add_argument('-l', type=str, help='ledger file')
+argp.add_argument('cmd', type=str, choices=['entry', 'asset'], help='subcommand')
+args = argp.parse_args()
+
+if args.v:
+ logg.setLevel(getattr(logging, args.v.upper()))
+
+ctx = Context(args)
diff --git a/dummy/usawa/runnable/create.py b/dummy/usawa/runnable/create.py
@@ -114,7 +114,6 @@ argp.add_argument('-u', '--unit', type=str, default=UnitIndex.default_unit, help
argp.add_argument('-o', type=str, dest='output', help='output file for updated XML document')
argp.add_argument('-l', type=str, dest='src_uri', help='URI for data source')
argp.add_argument('--unit-precision', type=int, default=UnitIndex.default_precision, help='Unit precision')
-argp.add_argument('--unit-rate', type=float, default=1.0, help='Unit exchange rate')
arg = argp.parse_args()
ctx = Context.from_args(arg)
diff --git a/dummy/usawa/store.py b/dummy/usawa/store.py
@@ -105,7 +105,7 @@ def pfx_asset(asset):
return PFX_ASSET + asset.get_digest(binary=True)
-class KeyStore(Interface):
+class BaseStore(Interface):
def __init__(self, implementation):
if not isinstance(implementation, Interface):
@@ -113,6 +113,20 @@ class KeyStore(Interface):
self.db = implementation
+ """Implements whee.Interface.put
+ """
+ def put(self, k, v):
+ return self.db.put(k, v)
+
+
+ """Implements whee.Interface.get
+ """
+ def get(self, k):
+ return self.db.get(k)
+
+
+class KeyStore(BaseStore):
+
"""Add signing key to the store.
If this is the first key in the store, it will be set as default.
@@ -166,16 +180,26 @@ class KeyStore(Interface):
return wallet_class.from_export(r, passphrase=passphrase)
- """Implements whee.Interface.put
- """
- def put(self, k, v):
- return self.db.put(k, v)
+class EntryStore(BaseStore):
- """Implements whee.Interface.get
- """
- def get(self, k):
- return self.db.get(k)
+ def put_draft(self, entry):
+ k = pfx_entry_draft(entry)
+ v = entry.serialize()
+ self.db.put(k, v, exist_ok=True)
+
+
+ def get_draft(self, entry):
+ k = pfx_entry_draft(entry)
+ v = self.db.get(k)
+ entry = Entry.deserialize(v)
+ # TODO: hacky!
+ i = 0
+ for o in entry.attachment:
+ asset = self.get_asset(o)
+ entry.attachment[i] = asset
+ i += 1
+ return entry
class LedgerStore(KeyStore):
"""Wrapper for an implementation of the whee store that handles encoding of ledgers and entries.
@@ -244,24 +268,6 @@ class LedgerStore(KeyStore):
self.ledger.add_entry(entry)
- def add_draft(self, entry):
- k = pfx_entry_draft(entry)
- v = entry.serialize()
- self.db.put(k, v)
-
-
- def get_draft(self, entry):
- k = pfx_entry_draft(entry)
- v = self.db.get(k)
- entry = Entry.deserialize(v)
- # TODO: hacky!
- i = 0
- for o in entry.attachment:
- asset = self.get_asset(o)
- entry.attachment[i] = asset
- i += 1
- return entry
-
"""Restore an entry from data from the store.