commit 84805b603dac3c924427681d08522c5fbf764491
parent 7a8ee65dc5c2fddfa6a6ca7a0d4a919a9a9f61b2
Author: lash <dev@holbrook.no>
Date: Fri, 13 Feb 2026 10:27:12 +0000
Add asset file parser
Diffstat:
5 files changed, 122 insertions(+), 0 deletions(-)
diff --git a/dummy/doc/server.texi b/dummy/doc/server.texi
@@ -0,0 +1,10 @@
+@anchor{service}
+@chapter Service
+
+usawa provides a socket daemon service to allow multiple clients operate on the same entry store concurrently.
+
+It is currently implemented as a @emph{Unix socket} and a @emph{TCP socket}.
+
+The daemon will be run with its own private key, and will sign all responses.
+
+The daemon will also provide an authentication mechanism, checking access to the store with an ACL and a signature for each request.
diff --git a/dummy/setup.py b/dummy/setup.py
@@ -9,6 +9,7 @@ varints_dir = os.path.join(aux_dir, 'varints', 'varints')
setup(
install_requires=[
+ "filemagic~=1.6",
"wheepy[valkey]~=0.0.3",
"confini~=0.6.5",
"lxml~=6.0.2",
diff --git a/dummy/tests/asset.py b/dummy/tests/asset.py
@@ -0,0 +1,22 @@
+import logging
+import os
+import unittest
+
+from usawa.asset import Asset
+
+logging.basicConfig(level=logging.DEBUG)
+logg = logging.getLogger()
+
+testdir = os.path.realpath(os.path.dirname(__file__))
+
+
+class TestAsset(unittest.TestCase):
+
+ def test_asset_file(self):
+ fp = os.path.join(testdir, 'test.xml')
+ asset = Asset.from_file(fp)
+ logg.debug('asset {}'.format(asset))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/dummy/usawa/asset.py b/dummy/usawa/asset.py
@@ -0,0 +1,87 @@
+import hashlib
+import mimetypes
+import logging
+import os
+
+import magic
+
+logg = logging.getLogger('asset')
+
+BLOCKSIZE = 512
+
+magic = magic.Magic(flags=magic.MAGIC_MIME_TYPE)
+
+def parse_path(path):
+ s = os.path.basename(path)
+ v = s.rsplit('.', maxsplit=1)
+ slug = v[0]
+ ext = None
+ if len(v) == 2:
+ ext = v[1]
+ return (slug, ext,)
+
+class Asset:
+
+ def __init__(self):
+ self.digest = None
+ self.mime = None
+ self.enc = None
+ self.slug = None
+ self.ext = None
+
+
+
+
+ def get_filename(self):
+ s = self.slug
+ if self.ext != None:
+ s += '.' + self.ext
+ return s
+
+
+ def get_mimestring(self):
+ s = self.mime
+ if self.enc != None:
+ s += '; encoding=' + self.enc
+ return s
+
+
+
+ @staticmethod
+ def from_file(filepath, description=None, slug=None, mimetype=None):
+ o = Asset()
+ h = hashlib.sha256()
+ f = open(filepath, 'rb')
+ b = f.read(BLOCKSIZE)
+ if mimetype == None:
+ v = mimetypes.guess_file_type(filepath, strict=True)
+ if v != None:
+ mimetype = v[0]
+ o.enc = v[1]
+ if mimetype == None:
+ mimetype = magic.id_buffer(b)
+ o.mime = mimetype
+ h.update(b)
+ c = BLOCKSIZE
+ while True:
+ b = f.read(BLOCKSIZE)
+ if len(b) == 0:
+ break
+ h.update(b)
+ c += len(b)
+ f.close()
+ o.digest = h.digest()
+
+ s = mimetypes.guess_extension(o.mime, strict=True)
+ if s != None:
+ o.ext = s[1:]
+
+ (o.slug, o.ext) = parse_path(filepath)
+
+ logg.debug('asset read {} bytes from path {} mime {}'.format(c, filepath, o.mime))
+
+ return o
+
+
+ def __str__(self):
+ return 'file ̈́' + self.get_filename() + ' mime ' + self.get_mimestring() + ' digest ' + self.digest.hex()
diff --git a/dummy/usawa/runnable/add.py b/dummy/usawa/runnable/add.py
@@ -28,6 +28,7 @@ class Context:
self.part = []
self.output = None
self.f = None
+ self.attachments = []
def close(self):
@@ -114,6 +115,7 @@ argp.add_argument('-r', type=str, help='external reference')
argp.add_argument('-s', type=str, dest='src_account', default='general', help='source account')
argp.add_argument('-t', type=str, dest='dst_account', default='general', help='destination account')
argp.add_argument('-a', type=str, dest='amount', help='source and destination amount')
+argp.add_argument('-x', type=str, dest='attachment', action='append', help='add file attachment')
argp.add_argument('-o', type=str, dest='output', help='output file for updated XML document')
argp.add_argument('--src-type', dest='src_type', type=str, choices=CATEGORIES, default='expense', help='source type')
argp.add_argument('--dst-type', dest='dst_type', type=str, choices=CATEGORIES, default='asset', help='dest type')