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