usawa

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

commit ed233df02eeecba0de1643d841d2169f651ebdb1
parent d92495c658f333431ae988a49c28c09d15e60725
Author: lash <dev@holbrook.no>
Date:   Mon, 11 May 2026 14:11:02 -0600

Correct extref handling, add recalculation command

Diffstat:
Mdummy/usawa/asset.py | 18+++++++-----------
Mdummy/usawa/balance.py | 4++--
Mdummy/usawa/base.py | 3++-
Mdummy/usawa/entry.py | 42++++++++++++++++++++++++++++++------------
Mdummy/usawa/resolve/base.py | 4+++-
Mdummy/usawa/runnable/import.py | 81+++++++++++++++++++++++--------------------------------------------------------
Adummy/usawa/runnable/recalc.py | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mdummy/usawa/store.py | 10+++++++---
Mdummy/usawa/tag.py | 5++++-
9 files changed, 125 insertions(+), 89 deletions(-)

diff --git a/dummy/usawa/asset.py b/dummy/usawa/asset.py @@ -75,6 +75,7 @@ class Asset(UsawaElement): self.extref = extref self.description = description + """Return the preferred filename with extension for the asset. Must always return a value. The filename may or may not have an extension. @@ -82,7 +83,6 @@ class Asset(UsawaElement): :return: Filename :rtype: str """ - def get_filename(self): s = self.slug if self.ext != None: @@ -218,8 +218,8 @@ class Asset(UsawaElement): h = hashlib.sha1() h.update(o.digest) - o.extref = extref o.description = description + o.extref = extref return o @@ -266,11 +266,6 @@ class Asset(UsawaElement): tree.set('ref', self.ref) - if self.extref != None: - o = lxml.etree.SubElement(tree, "extref") - o.text = self.extref - tree.append(o) - o = lxml.etree.SubElement(tree, "filename") o.text = self.get_filename() tree.append(o) @@ -303,9 +298,8 @@ class Asset(UsawaElement): v = tree.find("digest", namespaces=nsmap()).text o.digest = bytes.fromhex(v) - v = tree.find("extref", namespaces=nsmap()) - if v != None: - o.extref = v.text + for v in tree.findall("extref", namespaces=nsmap()): + o.add_extref(v.text) v = tree.find("filename", namespaces=nsmap()) if v != None: @@ -335,6 +329,7 @@ class Asset(UsawaElement): for k in ['mime', 'ref', 'slug', 'ext', 'description', 'extref', 'enc']: #for k in ["mime", "slug", "ext", "description", "extref", "enc"]: v = getattr(self, k) + logg.debug('serialize asset part {} {}'.format(k, v)) d.append(v) return d @@ -369,6 +364,7 @@ class Asset(UsawaElement): vv = vv.decode("utf-8") setattr(o, k, vv) i += 1 + logg.debug('deserialized asset part {} {}'.format(k, vv)) if isinstance(digest, str): digest = bytes.fromhex(digest) o.digest = digest @@ -376,7 +372,7 @@ class Asset(UsawaElement): def __str__(self): return ( - "file ̈́" + "file " + self.get_filename() + " mime " + self.get_mimestring() diff --git a/dummy/usawa/balance.py b/dummy/usawa/balance.py @@ -66,7 +66,7 @@ class Balancer: def _handle_asset(self, amount, issrc=False): if issrc: if amount >= 0: - raise ValueError('positive asset can only be dst') + logg.warning('positive asset should only be dst') self.r += amount return amount @@ -74,6 +74,6 @@ class Balancer: def _handle_liability(self, amount, issrc=False): if issrc: if amount < 0: - raise ValueError('negative liability can only be dst') + logg.warning('negative liability should only be dst') self.r -= amount return amount diff --git a/dummy/usawa/base.py b/dummy/usawa/base.py @@ -23,7 +23,8 @@ class UsawaElement: if ref == None: self.ref = str(uuid.uuid4()) else: - self.ref = str(uuid.UUID(ref)) + str(uuid.UUID(ref)) + self.ref = ref self.kv = {} diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py @@ -2,6 +2,7 @@ import enum import logging import datetime import hashlib +import uuid import lxml.etree import rencode @@ -66,6 +67,10 @@ class EntryPart: return self.account.to_path(display=mode) + def account_path_only(self): + return self.account.to_path(display=AccountDisplay.path) + + """Create object from an entry part defined as an XML tree. The XML expected is the ledger/entry/data/debit or ledger/entry/data/credit (in schema, defined as the EntryPart complexType). @@ -127,7 +132,8 @@ class EntryPart: d = [ self.account.get_unit(), self.account.get_type_str(), - self.account_path(with_unit=False), + #self.account_path(with_unit=False), + self.account_path_only(), self.amount, ] return d @@ -155,9 +161,10 @@ class EntryPart: def deserialize(data, debit=False): v = rencode.loads(data) unit = v[0].decode('utf-8') - typ = v[1].decode('utf-8') + typ = getattr(AccountType, v[1].decode('utf-8').lower()) account_path = v[2].decode('utf-8') - account = Account.from_path(account_path, sym=unit) + logg.debug('deserializing account path {}'.format(account_path)) + account = Account.from_path(account_path, sym=unit, typ=typ) amount = v[3] o = EntryPart(account, amount, debit=debit) return o @@ -337,6 +344,9 @@ class Entry(UsawaElement): raise ValueError('entry serial preceeds ledger') ref = o.find('ref', namespaces=nsmap()).text + extref = o.find('extref', namespaces=nsmap()) + if extref != None: + extref = extref.text parent = o.find('parent', namespaces=nsmap()).text description = o.find('description', namespaces=nsmap()) if description != None: @@ -344,15 +354,17 @@ class Entry(UsawaElement): dt = datetime.date.fromisoformat(o.find('date', namespaces=nsmap()).text) dtreg = datetime.datetime.strptime(o.find('dateTimeRegistered', namespaces=nsmap()).text, '%Y-%m-%dT%H:%M:%SZ') - src_tree = o.find('debit', namespaces=nsmap()) - dst_tree = o.find('credit', namespaces=nsmap()) + entry = Entry(serial, dt, ref=ref, parent=parent, tx_datereg=dtreg, description=description, unitindex=unitindex, extref=extref) + logg.debug('parent {} extref {}'.format(parent, extref)) - entry = Entry(serial, dt, ref=ref, parent=parent, tx_datereg=dtreg, description=description, unitindex=unitindex) - src = EntryPart.from_tree(src_tree, debit=True) - dst = EntryPart.from_tree(dst_tree) - entry.add_part(dst) - entry.add_part(src) + for dst_tree in o.findall('credit', namespaces=nsmap()): + dst = EntryPart.from_tree(dst_tree) + entry.add_part(dst) + for src_tree in o.findall('debit', namespaces=nsmap()): + src = EntryPart.from_tree(src_tree, debit=True) + entry.add_part(src) + for v in o.findall('attachment', namespaces=nsmap()): asset = Asset.from_tree(v) entry.attach(asset) @@ -442,13 +454,19 @@ class Entry(UsawaElement): extref = v[11] except IndexError: pass + if extref != None: + extref = extref.decode('utf-8') + logg.debug('extref {}'.format(extref)) date_reg = datetime.datetime.strptime(v[4].decode('utf-8'), '%Y%m%d%H%M%S') date = datetime.datetime.strptime(v[5].decode('utf-8'), '%Y%m%d%H%M%S') description = v[6].decode('utf-8') dst_data = v[7] src_data = v[8] attach_data = v[9] - tags = Tags.deserialize(v[10], store=store) + tags_data = v[10] + tags = None + if tags_data != None: + tags = Tags.deserialize(tags_data, store=store) #tags = None #try: # tags = Tags.deserialize(v[10]) @@ -461,7 +479,7 @@ class Entry(UsawaElement): # except AttributeError as e: # logg.debug('fail tag decode, use raw: {}'.format(s.hex())) # tags.append(s) - o = Entry(serial, date, ref=ref, description=description, parent=parent, tx_datereg=date_reg, tags=tags, unitindex=unitindex) + o = Entry(serial, date, ref=ref, description=description, parent=parent, tx_datereg=date_reg, tags=tags, unitindex=unitindex, extref=extref) super(Entry, o).deserialize(v[0]) if unitindex != None: o.balancer = Balancer(unitindex) diff --git a/dummy/usawa/resolve/base.py b/dummy/usawa/resolve/base.py @@ -110,7 +110,7 @@ class BaseResolver: return k - def restore_ledger(self, ledger, min=0): + def restore_ledger(self, ledger, min=0, entry_callback=None): lookup = self.get(ledger.current()) while True: entry = Entry.from_string(lookup, ledger.uidx) @@ -118,6 +118,8 @@ class BaseResolver: break logg.debug('restore entry {} {}'.format(str(entry), lookup)) k = entry.parent + if entry_callback != None: + entry_callback(entry) ledger.add_entry(entry, check_parent=False) if k == DEFAULTPARENT: break diff --git a/dummy/usawa/runnable/import.py b/dummy/usawa/runnable/import.py @@ -7,6 +7,7 @@ import uuid import datetime import usawa.config +from usawa.context import UsawaContext from usawa import Ledger, Entry, EntryPart, DemoWallet, load, ACL from usawa.constant import CATEGORIES from usawa.store import LedgerStore @@ -16,65 +17,29 @@ logging.basicConfig(level=logging.DEBUG) logg = logging.getLogger() -class Context: - - def __init__(self): - self.unit = None - self.uidx = None - self.output = None - self.f = None - self.valkey_host = None - self.valkey_port = None - - - def close(self): - if self.f and self.f != sys.stdout: - self.f.close() - - - def open(self, output): - if output == '<stdout>': - self.f = sys.stdout.buffer - logg.debug('output is stdout') - else: - self.f = open(output, 'wb') - return self - - @staticmethod - def from_args(args): - ctx = Context() - if args.output != None: - ctx.output = os.path.realpath(args.output) - else: - ctx.output = '<stdout>' - - ctx.valkey_host = args.valkey_host - ctx.valkey_port = args.valkey_port - - return ctx - - def main(): argp = argparse.ArgumentParser() - argp.add_argument('-o', type=str, dest='output', help='output file for resulting XML document') - argp.add_argument('--valkey-host', dest='valkey_host', type=str, default='localhost', help='Valkey host') - argp.add_argument('--valkey-port', dest='valkey_port', type=int, default=6379, help='Valkey port') - argp.add_argument('ledger_xml_file', type=str, help='load ledger metadata from XML file') - arg = argp.parse_args() - ctx = Context.from_args(arg) - - ledger = Ledger.from_file(arg.ledger_xml_file) - - cfg = usawa.config.load() - storedb = ValkeyStore('', host=ctx.valkey_host, port=ctx.valkey_port) - store = LedgerStore(storedb, ledger) - #pk = store.get_key() - #wallet = DemoWallet(privatekey=pk) - #acl = ACL.from_wallet(wallet) - #store.load(acl=acl) - store.put_all(store_assets=True) - sys.stdout.write(ledger.to_string()) - - + 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('-i', type=str, help='input ledger state') + argp.add_argument('-p', action='store_true', help='unlock wallet with password') + args = argp.parse_args() + + if args.v: + logg.setLevel(getattr(logging, args.v.upper())) + + cfg = usawa.config.load_config(config_dir=args.c) + ctx = UsawaContext(cfg) + ctx.init(args) + + def store_entry(entry): + ctx.store.add_entry(entry) + logg.debug('store entry {}'.format(entry)) + for v in entry.attachment: + ctx.store.add_asset(v, overwrite=True) + + ctx.resolver.restore_ledger(ctx.ledger, entry_callback=store_entry) + + if __name__ == '__main__': main() diff --git a/dummy/usawa/runnable/recalc.py b/dummy/usawa/runnable/recalc.py @@ -0,0 +1,47 @@ +import os +import sys +import logging +import urllib.parse +import argparse +import uuid +import datetime + +import usawa.config +from usawa.context import UsawaContext +from usawa import Ledger, Entry, EntryPart, DemoWallet, UnitIndex, load, ACL +from usawa.constant import CATEGORIES +from usawa.store import LedgerStore +from whee.valkey import ValkeyStore +from whee.fs import FsStore +from xdg_base_dirs import xdg_data_home + +logging.basicConfig(level=logging.WARNING) +logg = logging.getLogger() + + +def main(): + argp = argparse.ArgumentParser() + argp.add_argument('-o', type=str, dest='output', help='output file for resulting XML document') + #argp.add_argument('-p', action='store_true', help='prompt for password to open wallet') + argp.add_argument('-c', type=str, help='override config dir') + argp.add_argument('-v', type=str, choices=['info','debug','warning','error'], help='be verbose') + argp.add_argument('-i', type=str, help='input ledger state') + argp.add_argument('-p', action='store_true', help='unlock wallet with password') + arg = argp.parse_args() + + if arg.v: + logg.setLevel(getattr(logging, arg.v.upper())) + + cfg = usawa.config.load_config(config_dir=arg.c) + ctx = UsawaContext(cfg) + ctx.init(arg) + + def override_parent(entry): + entry.parent = ctx.ledger.cur + ctx.store.add_entry(entry, overwrite=True) + + ctx.store.load(entry_callback_pre=override_parent) + + +if __name__ == '__main__': + main() diff --git a/dummy/usawa/store.py b/dummy/usawa/store.py @@ -295,10 +295,10 @@ class EntryStore(KeyStore): :raises: ValueError if the entry is not the right object type. :raises: FileExistsError if entry is already in store. """ - def add_entry(self, entry, update_ledger=False): + def add_entry(self, entry, update_ledger=False, overwrite=False): k = pfx_entry(self.ledger, entry) v = entry.wrap() - self.db.put(k, v) + self.db.put(k, v, exist_ok=overwrite) if update_ledger: self.ledger.add_entry(entry) @@ -366,7 +366,7 @@ class LedgerStore(EntryStore, AssetStore, KeyStore): :raises FileNotFoundError: If an entry cannot be found. """ - def load(self, acl=None, until=0, unitindex=None): + def load(self, acl=None, until=0, unitindex=None, entry_callback_pre=None, entry_callback_post=None): logg.debug('load ledger from store {} until {}'.format(self.ledger, until)) v = 0 while True: @@ -381,7 +381,11 @@ class LedgerStore(EntryStore, AssetStore, KeyStore): except FileNotFoundError as e: logg.debug('entry serial {} not found, terminating ({})'.format(v, e)) break + if entry_callback_pre != None: + entry_callback_pre(o) self.ledger.add_entry(o) + if entry_callback_post != None: + entry_callback_post(o) logg.info('loaded ledger {}'.format(self.ledger)) diff --git a/dummy/usawa/tag.py b/dummy/usawa/tag.py @@ -39,8 +39,11 @@ class Tags: @staticmethod def deserialize(data, store=None): - r = rencode.loads(data) o = Tags(store=store) + try: + r = rencode.loads(data) + except TypeError: + return o for k in r: k = k.decode('utf-8') v = None