usawa

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

commit c19f23384b22acf5b22ea3bcdd11e02c37f56572
parent 501a53c1cff48d60d024d28c1a2afea02d9563d9
Author: lash <dev@holbrook.no>
Date:   Fri, 13 Feb 2026 12:38:33 +0000

Remove localref from attachment, add attachment support in entry

Diffstat:
Mdummy/tests/asset.py | 6++----
Mdummy/tests/entry.py | 18+++++++++++++++++-
Mdummy/usawa/__init__.py | 1+
Mdummy/usawa/asset.py | 50++++++++++++++++++++++++++------------------------
Mdummy/usawa/data/schema.xsd | 2--
Mdummy/usawa/entry.py | 25++++++++++++-------------
6 files changed, 58 insertions(+), 44 deletions(-)

diff --git a/dummy/tests/asset.py b/dummy/tests/asset.py @@ -23,21 +23,19 @@ class TestAsset(unittest.TestCase): def test_asset_export(self): fp = os.path.join(testdir, 'test.xml') - asset = Asset.from_file(fp, slug='foo', description='barbarbar', extref='xyzzy', localref='plugh') + asset = Asset.from_file(fp, slug='foo', description='barbarbar', extref='xyzzy') tree = asset.to_tree() logg.debug('asset {}'.format(lxml.etree.tostring(tree))) def test_asset_import(self): fp = os.path.join(testdir, 'test.xml') - asset = Asset.from_file(fp, slug='foo', description='barbarbar', extref='xyzzy', localref='plugh') + asset = Asset.from_file(fp, slug='foo', description='barbarbar', extref='xyzzy') tree = asset.to_tree() s = lxml.etree.tostring(tree) tree = lxml.etree.fromstring(s) o = Asset.from_tree(tree) - logg.debug('imported asset {}'.format(o)) - if __name__ == '__main__': diff --git a/dummy/tests/entry.py b/dummy/tests/entry.py @@ -4,7 +4,7 @@ import unittest import os import copy -from usawa import EntryPart, Entry, DemoWallet, ACL, UnitIndex +from usawa import EntryPart, Entry, DemoWallet, ACL, UnitIndex, Asset from usawa.error import ACLError import lxml.etree @@ -109,5 +109,21 @@ class TestEntry(unittest.TestCase): tree = Entry.from_tree(tree, self.uidx) + def test_entry_attach(self): + 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) + o.add_part(src, debit=True) + o.add_part(dst) + + fp = os.path.join(testdir, 'test.xml') + asset = Asset.from_file(fp) + o.attach(asset) + wallet = DemoWallet() + o.sign(wallet) + tree = o.to_tree() + logg.debug('entry tree with attachment {}'.format(lxml.etree.tostring(tree))) + + if __name__ == '__main__': unittest.main() diff --git a/dummy/usawa/__init__.py b/dummy/usawa/__init__.py @@ -7,6 +7,7 @@ from .entry import Entry, EntryPart from .crypto import DemoWallet, ACL from .xml import nsmap from .unit import UnitIndex +from .asset import Asset data_dir = os.path.join(os.path.dirname(__file__), 'data') diff --git a/dummy/usawa/asset.py b/dummy/usawa/asset.py @@ -55,7 +55,6 @@ class Asset: self.enc = None self.slug = None self.ext = None - self.localref = None self.extref = None self.uuid = None self.description = None @@ -108,9 +107,9 @@ class Asset: :raises PermissionError: File cannot be read. """ @staticmethod - def from_file(filepath, description=None, slug=None, mimetype=None, localref=None, extref=None): + def from_file(filepath, description=None, slug=None, mimetype=None, extref=None): f = open(filepath, 'rb') - return Asset.from_io(f, filepath, closer=f.close, description=description, slug=slug, mimetype=mimetype, localref=localref, extref=extref) + return Asset.from_io(f, filepath, closer=f.close, description=description, slug=slug, mimetype=mimetype, extref=extref) """Instantiate an asset object from an input stream. @@ -129,8 +128,6 @@ class Asset: :type slug: str :param mimetype: Explicitly set mime type to this value. :type mimetype: str - :param localref: A local reference (e.g. invoice number). - :type localref: str :param extref: An external reference (e.g. invoice number). :type extref: str :todo: make sure stream close on exception. @@ -138,7 +135,7 @@ class Asset: :todo: document possible exceptions. """ @staticmethod - def from_io(io, src, closer=None, description=None, slug=None, mimetype=None, localref=None, extref=None): + def from_io(io, src, closer=None, description=None, slug=None, mimetype=None, extref=None): o = Asset() h = hashlib.sha256() b = io.read(BLOCKSIZE) @@ -173,40 +170,46 @@ class Asset: logg.debug('asset read {} bytes from path {} mime {}'.format(c, src, o.mime)) o.uuid = str(uuid.uuid4()) - if localref == None: - localref = o.uuid - o.localref = localref o.extref = extref o.description = description return o - """Generate and return an XML representation of the asset. + """Return canonical XML for use in signature message calculation. - :returns: XML tree representing the asset. - :rtype: lxml.etree.Element - :todo: implement sigs + Elements in canonical XML for the asset is data that can be recreated by the actual file data. + + Although MIME type may be ambigious, it can reasonably be guessed by scanning the asset data after the fact and most likely with trial-and-error from the results of that operation. + + For custom or arcane MIME types, the data needed to recreate the MIME type has to be retained outside the application. """ - def to_tree(self): + def canon(self): tree = lxml.etree.Element(NSPREFIX + 'attachment', nsmap=nsmap()) if self.mime != None: tree.set('mime', self.get_mimestring()) - if self.uuid != None: - tree.set('uuid', self.uuid) o = lxml.etree.SubElement(tree, 'digest') + o.set('algo', 'sha256') o.text = self.digest.hex() tree.append(o) - o = lxml.etree.SubElement(tree, 'lookup') - o.text = '.' - o.set('method', 'local') - tree.append(o) + return tree - o = lxml.etree.SubElement(tree, 'ref') - o.text = self.localref - tree.append(o) + + """Generate and return an XML representation of the asset. + + :returns: XML tree representing the asset. + :rtype: lxml.etree.Element + :todo: implement sigs + """ + def to_tree(self, canon=False): + tree = self.canon() + if canon: + return tree + + if self.uuid != None: + tree.set('uuid', self.uuid) if self.extref != None: o = lxml.etree.SubElement(tree, 'extref') @@ -243,7 +246,6 @@ class Asset: o.mime = tree.get('mime') v = tree.find('digest', namespaces=nsmap()).text o.digest = bytes.fromhex(v) - o.ref = tree.find('ref', namespaces=nsmap()).text v = tree.find('extref', namespaces=nsmap()) if v != None: diff --git a/dummy/usawa/data/schema.xsd b/dummy/usawa/data/schema.xsd @@ -157,8 +157,6 @@ <xs:complexType name="Attachment"> <xs:sequence> <xs:element name="digest" type="Digest" /> - <xs:element name="lookup" type="Lookup" /> - <xs:element name="ref" type="xs:string" /> <xs:element name="extref" type="xs:string" minOccurs="0" maxOccurs="1" /> <xs:element name="filename" type="xs:string" minOccurs="0" maxOccurs="1" /> <xs:element name="description" type="xs:string" minOccurs="0" maxOccurs="1" /> diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py @@ -183,19 +183,13 @@ class Entry: """Append a single media asset to the attachment list for the entry. - :param mime: MIME type of asset. - :type mime: str - :param algo: Algorithm used to generate digest of attachment. Must be a valid algorithm identifier for hashlib (standard library). - :type algo: str - :param digest: The digest of the asset. - :type digest: bytes - :param description: Optional description string for the asset. - :type description: str - :param slug: Optional machine-friendly name, e.g. used as filename stem. - :type slug: str + Does not detect duplicate inserts (inserts with same digest). + + :param asset: The asset to add. + :type asset: usawa.Asset """ - def attach(self, mime, algo, digest, description=None, slug=None): - self.attachment.append((mime, algo, digest, description, slug,)) + def attach(self, asset): + self.attachment.append(asset) """Add a signature over the ledger state of the entry. @@ -477,7 +471,11 @@ class Entry: for v in self.credit: o = v.to_tree() data.append(o) - + + for v in self.attachment: + o = v.to_tree() + data.append(o) + tree.append(data) for k in self.sigs.keys(): @@ -495,6 +493,7 @@ class Entry: :return: Signature material. :rtype: str + :todo: replace attachment list with only non-optional parts for signature. """ def canon(self): tree = self.to_tree()