usawa

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

commit 20303dfe4869b48a6f21d97db044ce05f81d3588
parent fe1638302dd048dd8be32f729ccbed9b5190ebd3
Author: lash <dev@holbrook.no>
Date:   Sun, 22 Mar 2026 16:58:42 -0600

Add base element with tags and key-value metadata

Diffstat:
Mdummy/usawa/asset.py | 24+++++++++++-------------
Mdummy/usawa/data/schema.xsd | 4++--
Mdummy/usawa/entry.py | 38++++++++++++++++++++++++--------------
Mdummy/usawa/gui/__init__.py | 3++-
Mdummy/usawa/gui/main_window.py | 6+++++-
Mdummy/usawa/gui/views/create_entry_view.py | 7++++---
Mdummy/usawa/gui/views/entry_list_view.py | 4+++-
Mdummy/usawa/ledger.py | 3++-
Mdummy/usawa/runnable/gui.py | 4++++
Mdummy/usawa/store.py | 1+
10 files changed, 58 insertions(+), 36 deletions(-)

diff --git a/dummy/usawa/asset.py b/dummy/usawa/asset.py @@ -2,12 +2,12 @@ import hashlib import mimetypes import logging import os -import uuid import lxml.etree import rencode import magic +from .base import UsawaElement from .constant import NSPREFIX from .xml import nsmap @@ -46,7 +46,7 @@ def parse_path(path): ) -class Asset: +class Asset(UsawaElement): """Represents a file asset, used as attachment in usawa.Entry. Object is not intended to be instantiated directly. Instead one of the following static methods should be used: @@ -56,14 +56,14 @@ class Asset: Asset.from_tree() - Recreate from an XML tree. """ - def __init__(self, digest=None): + def __init__(self, digest=None, ref=None): + super(Asset, self).__init__(ref=ref) self.digest = digest self.mime = None self.enc = None self.slug = None self.ext = None self.extref = None - self.uuid = None self.description = None """Return the preferred filename with extension for the asset. @@ -202,7 +202,6 @@ class Asset: h = hashlib.sha1() h.update(o.digest) - # o.uuid = str(uuid.uuid1(h.digest())) o.extref = extref o.description = description @@ -244,8 +243,7 @@ class Asset: if canon: return tree - # if self.uuid != None: - # tree.set('uuid', self.uuid) + tree.set('ref', self.ref) if self.extref != None: o = lxml.etree.SubElement(tree, "extref") @@ -278,8 +276,8 @@ class Asset: @staticmethod def from_tree(tree): - o = Asset() - # o.uuid = tree.get('uuid') + uuid = tree.get('ref') + o = Asset(ref=uuid) o.mime = tree.get("mime") v = tree.find("digest", namespaces=nsmap()).text o.digest = bytes.fromhex(v) @@ -312,8 +310,8 @@ class Asset: def to_list(self): d = [] - # for k in ['mime', 'uuid', 'slug', 'ext', 'description', 'extref', 'enc']: - for k in ["mime", "slug", "ext", "description", "extref", "enc"]: + for k in ['mime', 'ref', 'slug', 'ext', 'description', 'extref', 'enc']: + #for k in ["mime", "slug", "ext", "description", "extref", "enc"]: v = getattr(self, k) d.append(v) return d @@ -341,8 +339,8 @@ class Asset: o = Asset() v = rencode.loads(data) i = 0 - # for k in ['mime', 'uuid', 'slug', 'ext', 'description', 'extref', 'enc']: - for k in ["mime", "slug", "ext", "description", "extref", "enc"]: + for k in ['mime', 'ref', 'slug', 'ext', 'description', 'extref', 'enc']: + #for k in ["mime", "slug", "ext", "description", "extref", "enc"]: vv = v[i] if isinstance(vv, bytes): vv = vv.decode("utf-8") diff --git a/dummy/usawa/data/schema.xsd b/dummy/usawa/data/schema.xsd @@ -130,7 +130,7 @@ <xs:element name="data" type="EntryData" /> <xs:element name="sig" type="Signature" minOccurs="1" maxOccurs="unbounded" /> </xs:sequence> - <xs:attribute name="uuid" type="xs:string" /> + <xs:attribute name="ref" type="xs:string" /> </xs:complexType> <xs:complexType name="ParentRef"> @@ -173,7 +173,7 @@ <xs:element name="sig" type="Signature" minOccurs="0" maxOccurs="unbounded" /> </xs:sequence> <xs:attribute name="mime" type="xs:string" /> - <xs:attribute name="uuid" type="xs:string" /> + <xs:attribute name="ref" type="xs:string" /> </xs:complexType> <xs:simpleType name="accountType" final="restriction"> diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py @@ -1,12 +1,12 @@ import enum import logging import datetime -import uuid import hashlib import lxml.etree import rencode +from .base import UsawaElement from .constant import DEFAULTPARENT, NSPREFIX from .crypto import DemoWallet from .error import ACLError, VerifyError @@ -150,7 +150,7 @@ class EntryPart: return '[{}] {}:{} {}'.format(pfx, self.typ, self.account, self.amount) -class Entry: +class Entry(UsawaElement): digest_algo = 'sha512' # The algorithm used for generating entry digests. Must match a hashlib (standard library) type string identifier. @@ -181,15 +181,13 @@ class Entry: :todo: Ensure canonical format of keyid. """ def __init__(self, serial, tx_date, ref=None, description=None, parent=None, tx_datereg=None, unitindex=None): + super(Entry, self).__init__(ref=ref) if isinstance(parent, str): parent = bytes.fromhex(parent) elif parent == None: parent = DEFAULTPARENT elif len(parent) != 64: raise ValueError('invalid parent hash') - if ref == None: - ref = str(uuid.uuid4()) - self.ref = ref self.parent = parent self.serial = serial self.dt = tx_date @@ -207,6 +205,14 @@ class Entry: self.lookup = None self.lookup_algo = None + """Get the unique non-sum identifier of the entry as a string value. + + :returns: ref + :rtype: str + """ + def get_ref(self): + return self.ref + """Add an entry part to the entry. @@ -328,8 +334,10 @@ class Entry: for v in self.attachment: attach.append(v.get_digest(binary=True)) + base = super(Entry, self).serialize() logg.debug('serializing with parent {}'.format(self.parent.hex())) d = [ + base, self.parent, self.serial, self.ref, @@ -364,17 +372,19 @@ class Entry: def deserialize(data): v = rencode.loads(data) #parent = v[0].hex() - parent = v[0] - serial = v[1] - ref = v[2].decode('utf-8') - date_reg = datetime.datetime.strptime(v[3].decode('utf-8'), '%Y%m%d%H%M%S') - date = datetime.datetime.strptime(v[4].decode('utf-8'), '%Y%m%d') + parent = v[1] + serial = v[2] + ref = v[3].decode('utf-8') + 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') #unit = v[5].decode('utf-8') - description = v[5].decode('utf-8') - dst_data = v[6] - src_data = v[7] - attach_data = v[8] + description = v[6].decode('utf-8') + dst_data = v[7] + src_data = v[8] + attach_data = v[9] o = Entry(serial, date, ref=ref, description=description, parent=parent, tx_datereg=date_reg) + logg.debug('tup {}'.format(v[0])) + super(Entry, o).deserialize(v[0]) for v in src_data: #src = EntryPart(v[0].decode('utf-8'), v[1].decode('utf-8'), v[2].decode('utf-8'), v[3], debit=True) src = EntryPart.deserialize(v, debit=True) diff --git a/dummy/usawa/gui/__init__.py b/dummy/usawa/gui/__init__.py @@ -21,11 +21,12 @@ class Usawa(Adw.Application): self.win = None self.ledger_file = None self.cfg = None + self.account_list = None self.connect("activate", self.on_activate) signal.signal(signal.SIGINT, self._handle_sigint) def on_activate(self, app): - self.win = UsawaMainWindow(application=app, ledger_path=self.ledger_file) + self.win = UsawaMainWindow(application=app, ledger_path=self.ledger_file, account_list=self.account_list) self.win.present() def _handle_sigint(self, *_): diff --git a/dummy/usawa/gui/main_window.py b/dummy/usawa/gui/main_window.py @@ -24,8 +24,11 @@ logg = logging.getLogger("gui.mainwindow") class UsawaMainWindow(Adw.ApplicationWindow): - def __init__(self, application, ledger_path=None, **kwargs): + def __init__(self, application, ledger_path=None, account_list=None, **kwargs): super().__init__(application=application, **kwargs) + logg.debug('foo') + + self.account_list = account_list self.set_title("Usawa") self.set_default_size(1000, 600) @@ -118,6 +121,7 @@ class UsawaMainWindow(Adw.ApplicationWindow): entries=entries, refresh_callback=self.refresh_entries, toast_overlay=self.toast_overlay, + account_list=self.account_list, ) self.entry_list_view._load_entries() page.set_child(self.entry_list_view) diff --git a/dummy/usawa/gui/views/create_entry_view.py b/dummy/usawa/gui/views/create_entry_view.py @@ -6,11 +6,11 @@ import mimetypes logg = logging.getLogger("gui.create_entry_view") -def create_entry_page(nav_view, controller): +def create_entry_page(nav_view, controller, account_list=None): """Create a new entry page""" page = Adw.NavigationPage(title="Create New Entry", tag="create-entry") - view = CreateEntryView(nav_view, controller) + view = CreateEntryView(nav_view, controller, account_list=account_list) page.set_child(view) return page @@ -19,11 +19,12 @@ def create_entry_page(nav_view, controller): class CreateEntryView(Gtk.Box): """Create entry view - UI ONLY""" - def __init__(self, nav_view, controller): + def __init__(self, nav_view, controller, account_list=None): super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0) self.nav_view = nav_view self.controller = controller + self.account_list = account_list self.attachment_paths: list[str] = [] self._build_ui(nav_view) diff --git a/dummy/usawa/gui/views/entry_list_view.py b/dummy/usawa/gui/views/entry_list_view.py @@ -19,6 +19,7 @@ class EntryListView(Gtk.Box): toast_overlay, entries=None, refresh_callback=None, + account_list=None, **kwargs, ): super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0, **kwargs) @@ -28,6 +29,7 @@ class EntryListView(Gtk.Box): self.entries = entries or [] self.refresh_callback = refresh_callback self.toast_overlay = toast_overlay + self.account_list = account_list self.active_filter = None overlay = Gtk.Overlay() @@ -319,7 +321,7 @@ class EntryListView(Gtk.Box): def on_fab_clicked(self, button): logg.info("FAB clicked - opening create entry window") - create_page = create_entry_page(self.nav_view, self.entry_controller) + create_page = create_entry_page(self.nav_view, self.entry_controller, account_list=self.account_list) self.nav_view.push(create_page) def refresh_data(self): diff --git a/dummy/usawa/ledger.py b/dummy/usawa/ledger.py @@ -9,6 +9,7 @@ import lxml.etree import rencode import varints.leb128s +from .base import UsawaElement from .crypto import DemoWallet, ACL from .xml import nsmap, XML_FORMAT_VERSION from .constant import NSPREFIX, DEFAULTPARENT @@ -211,7 +212,7 @@ class RunningTotal: return 'running total {}: income {} expense {} asset {} liability {}'.format(self.sym, self.income, self.expense, self.asset, self.liability) -class Ledger: +class Ledger(UsawaElement): default_src = 'defalsify.org' diff --git a/dummy/usawa/runnable/gui.py b/dummy/usawa/runnable/gui.py @@ -9,3 +9,7 @@ logging.basicConfig(level=logging.DEBUG) def main(): app = Usawa(application_id='org.usawa.app') app.run(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/dummy/usawa/store.py b/dummy/usawa/store.py @@ -16,6 +16,7 @@ PFX_LEDGER_LOCK = b'\x03' PFX_ENTRY = b'\x04' PFX_UNIT_INDEX = b'\x08' PFX_ASSET = b'\x10' +PFX_AUX = b'\xfe' logg = logging.getLogger('usawa.store')