usawa

Unnamed repository; edit this file 'description' to name the repository.
Info | Log | Files | Refs | Submodules | LICENSE

commit aa838eedc4a4fb6ff5e01e853efbddd0fbc95518
parent 055d703a1c30762d046570766b00508fe82558c0
Author: lash <dev@holbrook.no>
Date:   Fri, 13 Feb 2026 18:01:54 +0000

Add store support for entry attachments

Diffstat:
Mdummy/tests/store.py | 37+++++++++++++++++++++++++++++++------
Mdummy/usawa/asset.py | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mdummy/usawa/crypto.py | 10+++++-----
Mdummy/usawa/entry.py | 61+++++++++++++++++++++++++++++++++++++++++++++----------------
Mdummy/usawa/store.py | 77+++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
5 files changed, 198 insertions(+), 55 deletions(-)

diff --git a/dummy/tests/store.py b/dummy/tests/store.py @@ -8,7 +8,7 @@ import uuid import lxml.etree from whee.mem import MemStore -from usawa import Ledger, UnitIndex, EntryPart, Entry, DemoWallet +from usawa import Ledger, UnitIndex, EntryPart, Entry, DemoWallet, Asset from usawa.store import LedgerStore from usawa.crypto import ACL @@ -40,16 +40,42 @@ class TestStore(unittest.TestCase): wallet = DemoWallet() o.sign(wallet) store.add_entry(o) - r = store.get_entry(o.serial) + + acl = ACL.from_wallet(wallet) + r = store.get_entry(o.serial, acl=acl) + self.assertEqual(r.ref, o.ref) + self.assertEqual(r.description, o.description) + + + def test_store_entry_attach(self): + uidx = UnitIndex('FOO') + ledger = Ledger(uidx, serial=42, base=self.parent) + store = LedgerStore(self.store, ledger) + dst = EntryPart('FOO', 'asset', 'foo', 1337) + src = EntryPart('FOO', 'income', 'foo', 1337, debit=True) + o = Entry(42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg, unitindex=uidx) + o.add_part(src, debit=True) + o.add_part(dst) + + fp = os.path.join(testdir, 'test.xml') + asset = Asset.from_file(fp, description='foobar') + store.add_asset(asset) + o.attach(asset) + + wallet = DemoWallet() + o.sign(wallet) + store.add_entry(o) + + acl = ACL.from_wallet(wallet) + r = store.get_entry(o.serial, acl=acl) self.assertEqual(r.ref, o.ref) self.assertEqual(r.description, o.description) + self.assertEqual(r.attachment[0].description, 'foobar') def test_store_ledger(self): uidx = UnitIndex('FOO') wallet = DemoWallet() - #acl = ACL() - #acl.add(wallet.pubkey()) acl = ACL.from_wallet(wallet) ledger = Ledger(uidx, serial=41, base=self.parent, acl=acl, wallet=wallet) store = LedgerStore(self.store, ledger) @@ -75,8 +101,7 @@ class TestStore(unittest.TestCase): store.add_entry(o) ledger.sign() - #ledger.reset() - store.load() + store.load(acl=acl) if __name__ == '__main__': diff --git a/dummy/usawa/asset.py b/dummy/usawa/asset.py @@ -5,6 +5,7 @@ import os import uuid import lxml.etree +import rencode import magic from .constant import NSPREFIX @@ -49,8 +50,8 @@ class Asset: Asset.from_io() - Read from a io.BufferedIOBase input stream. Asset.from_tree() - Recreate from an XML tree. """ - def __init__(self): - self.digest = None + def __init__(self, digest=None): + self.digest = digest self.mime = None self.enc = None self.slug = None @@ -91,6 +92,20 @@ class Asset: return s + """Return the digest of the asset, in hex. + + :raises AttributeError: Digest not set + :return: Digest hex + :rtype: str + """ + def get_digest(self, binary=False): + if self.digest == None: + raise AttributeError('') + if binary: + return self.digest + return self.digest.hex() + + """Instantiate an asset object from a local file. File is opened and the file object is passed on to the from_io() method. @@ -270,5 +285,54 @@ class Asset: return o + """Generate the simple data structure used for rencode serialization. + + :returns: data structure + :rtype: list + """ + def to_list(self): + d = [ + self.mime, + self.uuid, + self.extref, + self.slug, + self.ext, + self.description, + ] + return d + + + """Generate the serialization format used to calculate the digest for the asset. + + :returns: String representation of the entry, in rencode format. + :rtype: str + """ + def serialize(self): + b = self.to_list() + return rencode.dumps(b) + + + """Create an entry object from serialized data. + + :param data: rencoded entry object, as produced by the serialize() method. + :type data: str + :returns: Entry object. + :rtype: usawa.Entry + """ + @staticmethod + def deserialize(data, digest): + o = Asset() + v = rencode.loads(data) + i = 0 + for k in ['mime', 'uuid', 'extref', 'slug', 'ext', 'description']: + if v[i] != None: + setattr(o, k, v[i].decode('utf-8')) + i += 1 + if isinstance(digest, str): + digest = bytes.fromhex(digest) + o.digest = digest + return o + + def __str__(self): return 'file ̈́' + self.get_filename() + ' mime ' + self.get_mimestring() + ' digest ' + self.digest.hex() diff --git a/dummy/usawa/crypto.py b/dummy/usawa/crypto.py @@ -143,7 +143,7 @@ class Wallet: :rtype: boolean """ def verify(self, v, sig): - raise NotImplementedError + raise NotImplementedError """Generate an identity XML tree entry from the wallet. @@ -285,8 +285,8 @@ class ACL: :type did: usawa.DID """ def add(self, who, what=None, label=None, did=DEFAULT_DID): - if isinstance(who, bytes): - who = who.hex() + if isinstance(who, str): + who = bytes.fromhex(who) if label == None: label = who if what == None: @@ -304,8 +304,8 @@ class ACL: :rtype: boolean """ def have(self, who): - if isinstance(who, bytes): - who = who.hex() + if isinstance(who, str): + who = bytes.fromhex(who) return self.rev[who] diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py @@ -190,7 +190,7 @@ class Entry: :type asset: usawa.Asset """ def attach(self, asset): - logg.debug('attach {} to {}'.format(asset, self)) + logg.debug('attach {} to {}'.format(asset.get_digest(), self)) self.attachment.append(asset) @@ -253,6 +253,7 @@ class Entry: return entry + """Generate the simple data structure used for rencode serialization. :returns: data structure @@ -261,12 +262,16 @@ class Entry: def to_list(self): debit = [] credit = [] + attach = [] for v in self.debit: debit.append((v.unit, v.typ, v.account, v.amount,)) for v in self.credit: credit.append((v.unit, v.typ, v.account, v.amount,)) + for v in self.attachment: + attach.append(v.get_digest(binary=True)) + d = [ self.parent, self.serial, @@ -276,6 +281,7 @@ class Entry: self.description, debit, credit, + attach, ] return d @@ -309,6 +315,7 @@ class Entry: description = v[5].decode('utf-8') src_data = v[6] dst_data = v[7] + attach_data = v[8] o = Entry(serial, date, ref=ref, description=description, parent=parent, tx_datereg=date_reg) 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) @@ -317,8 +324,13 @@ class Entry: for v in dst_data: dst = EntryPart(v[0].decode('utf-8'), v[1].decode('utf-8'), v[2].decode('utf-8'), v[3]) o.add_part(dst) - logg.debug('deserialized entry {}'.format(o)) + + for v in attach_data: + asset = Asset(digest=v) + o.attach(asset) + logg.debug('deserialized entry {}'.format(o)) + return o @@ -408,30 +420,47 @@ class Entry: @staticmethod def unwrap(data, acl=None): v = rencode.loads(data) + entry = Entry.deserialize(v[2]) pubkey_bytes = v[0][0][1] - if acl != None: - label = None - try: - label = acl.have(pubkey_bytes) - except KeyError: - raise ACLError() - if not acl.may(label, 0x01): - raise ACLError() - wallet = DemoWallet(publickey=pubkey_bytes) sig = v[1][0] - entry = Entry.deserialize(v[2]) entry.add_signature(pubkey_bytes, sig) - entry.verify(wallet) + + if acl == None: + return entry + + label = None + try: + label = acl.have(pubkey_bytes) + except KeyError: + raise ACLError() + if not acl.may(label, 0x01): + raise ACLError() + wallet = DemoWallet(publickey=pubkey_bytes) + entry.verify(wallet, acl=acl) + return entry - def verify(self, wallet): + def verify(self, wallet=None, acl=None): + if wallet == None and acl == None: + raise ValueError('verify needs at least one of wallet or acl') (z, b) = self.sum() pubkeys = list(self.sigs.keys()) sig = self.sigs[pubkeys[0]] - if not wallet.verify(z, sig): + if wallet != None: + if wallet.verify(z, sig): + return + have = False + if acl != None: + for pubkey in acl.pubkeys(binary=True): + logg.debug('publickey acl {}'.format(pubkey)) + wallet = DemoWallet(publickey=pubkey) + sig = self.sigs[pubkey.hex()] + if wallet.verify(z, sig): + have = True + break + if not have: raise VerifyError() - # TODO: demo only takes into account single signature """Generate and return an XML representation of the entry. diff --git a/dummy/usawa/store.py b/dummy/usawa/store.py @@ -6,12 +6,14 @@ from whee import Interface from .ledger import Ledger from .entry import Entry +from .asset import Asset PFX_KEY = b'\x00' PFX_LEDGER = b'\x01' PFX_LEDGER_LOCK = b'\x02' PFX_ENTRY = b'\x04' +PFX_ASSET = b'\x10' logg = logging.getLogger('usawa.store') @@ -40,13 +42,6 @@ def pfx_key(pubkey=None): :rtype: bytes """ def pfx_ledger_topic(topic): - """Return ledger store prefix for topic. - - :params topic: Topic to generate prefix for. - :type topic: bytes - :returns: Prefix. - :rtype: bytes - """ r = PFX_LEDGER + topic return r @@ -59,13 +54,6 @@ def pfx_ledger_topic(topic): :rtype: bytes """ def pfx_ledger_lock(topic): - """Return ledger store locking prefix for topic. - - :params topic: Topic to generate prefix for. - :type topic: bytes - :returns: Prefix. - :rtype: bytes - """ r = PFX_LEDGER_LOCK + topic return r @@ -80,15 +68,6 @@ def pfx_ledger_lock(topic): :rtype: bytes """ def pfx_entry(ledger, entry): - """Return ledger store prefix for an entry. - - :param ledger: Ledger context for the entry. - :type ledger: usawa.Ledger - :param entry: Entry or serial to create prefix for. - :type entry: usawa.Entry or int - :returns: Prefix. - :rtype: bytes - """ serial = 0 if isinstance(entry, Entry): serial = entry.serial @@ -101,6 +80,16 @@ def pfx_entry(ledger, entry): return PFX_LEDGER + ledger.topic + serial.to_bytes(8, byteorder='big') +"""DB key prefix for adding entry attachment asset to a ledger. + +""" +def pfx_asset(asset): + if not isinstance(asset, Asset): + raise ValueError('invalid asset') + return PFX_ASSET + asset.get_digest(binary=True) + + + class LedgerStore(Interface): """Wrapper for an implementation of the whee store that handles encoding of ledgers and entries. @@ -181,11 +170,43 @@ class LedgerStore(Interface): :raises: PermissionError if the entry does not have a valid signature. :raises: ValueError if the serial number cannot be retrieved from the entry argument. :raises: FileExistsError if entry is already in store. + :todo: optimize replacing asset stub with deserialized asset """ def get_entry(self, entry, acl=None): k = pfx_entry(self.ledger, entry) v = self.__o.get(k) - return Entry.unwrap(v, acl=acl) + entry = Entry.unwrap(v) + # TODO: hacky! + i = 0 + for o in entry.attachment: + logg.debug('getentry ' + o.get_digest()) + asset = self.get_asset(o) + #asset = Asset.deserialize(v, digest=o.get_digest(binary=True)) + entry.attachment[i] = asset + i += 1 + entry.verify(acl=acl) + return entry + + + """Add an entry attachment asset to the store. + + :param asset: Asset containing digest to restore. + :type asset: usawa.Asset + :raises: FileExistsError if entry is already in store. + """ + def add_asset(self, asset): + k = pfx_asset(asset) + v = asset.serialize() + self.__o.put(k, v) + + + """Restore an entry attachment asset from the store. + """ + def get_asset(self, asset): + k = pfx_asset(asset) + v = self.__o.get(k) + digest = asset.get_digest(binary=True) + return Asset.deserialize(v, digest) """Flush ledger and load all entries from store. @@ -196,12 +217,12 @@ class LedgerStore(Interface): :raises FileNotFoundError: If an entry cannot be found. """ - def load(self): + def load(self, acl=None): logg.debug('load ledger from store {}'.format(self.ledger)) while True: o = None try: - o = self.get_entry(self.ledger.next_serial()) + o = self.get_entry(self.ledger.next_serial(), acl=acl) except FileNotFoundError: break self.ledger.add_entry(o) @@ -247,9 +268,13 @@ class LedgerStore(Interface): return self.__o.get(k) + """Implements whee.Interface.put + """ def put(self, k, v): return self.__o.put(k, v) + """Implements whee.Interface.get + """ def get(self, k): return self.__o.get(k)