usawa

Signed, immutable accounting.
Log | Files | Refs | Submodules | LICENSE

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:
Mdummy/tests/store.py | 2+-
Mdummy/usawa/account.py | 24+++++++++++++++++++++++-
Adummy/usawa/base.py | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdummy/usawa/data/usawa.ini | 6+++++-
Mdummy/usawa/entry.py | 6++++++
Adummy/usawa/runnable/craft.py | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdummy/usawa/runnable/create.py | 1-
Mdummy/usawa/store.py | 60+++++++++++++++++++++++++++++++++---------------------------
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.