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:
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