usawa

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

commit 93638ab8d231912e1226acb00ce1aebbf6fa7a07
parent 452035fba633b71198cb3c1529704345a9275c04
Author: lash <dev@holbrook.no>
Date:   Tue, 17 Feb 2026 07:08:43 +0000

Merge branch 'master' into lash/resolvers

Diffstat:
Mdummy/doc/internals.texi | 4++--
Adummy/tests/import.xml | 2++
Mdummy/tests/store.py | 17+++++++++++++++++
Mdummy/usawa/entry.py | 11+++++++++++
Mdummy/usawa/ledger.py | 11+++++++++++
Mdummy/usawa/runnable/add.py | 10+++++++++-
Adummy/usawa/runnable/import.py | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdummy/usawa/runnable/view.py | 10+++++++++-
Mdummy/usawa/store.py | 22++++++++++++++++++++++
9 files changed, 156 insertions(+), 4 deletions(-)

diff --git a/dummy/doc/internals.texi b/dummy/doc/internals.texi @@ -9,8 +9,6 @@ Signatures are calculated and embedded on three elements individually, while at The following describes any transformation applied to the XML. Also, any @code{sig} elements are removed before . -Where expendient, @url{https://dwarfstd.org/doc/Dwarf3.pdf,LEB128s} encoding is used for variable-length integer encoding. - @anchor{serialize_attachment} @subsection Attachment @@ -49,6 +47,8 @@ The lookup key descriptions below are enumerated. Each element in the list shoul Any optional and undefined elements @emph{must} contain a @emph{null value} in the serialization. +Where expendient, @url{https://dwarfstd.org/doc/Dwarf3.pdf,LEB128s} encoding is used for variable-length integer encoding. + @xref{store,Cache store} for more details. @subsection Ledger diff --git a/dummy/tests/import.xml b/dummy/tests/import.xml @@ -0,0 +1 @@ +<ledger xmlns="http://usawa.defalsify.org/" version="1"><topic>66a739edb189684585bde211f9c29f3a47616584cbe82175f88cb4a6329f9748aea04553db62e5b1bfbd7d121356e91fe2c6142a3d2ec9664099d0be203b87e4</topic><generated>2026-02-14T09:11:33Z</generated><src>defalsify.org</src><units base="BTC"><unit sym="BTC"><precision>2</precision><exchange>1000000000</exchange></unit></units><identity keyid="3b54648d60bb8a5b9e84fa0057f79b3a5996e511682e80176dc948dcbff5a4fc" didtype="usawa"/><incoming serial="0"><real unit="BTC"><income>13370000</income><expense>421300</expense><asset>13370000</asset><liability>421300</liability></real><digest algo="sha512">00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000</digest><sig keyid="3b54648d60bb8a5b9e84fa0057f79b3a5996e511682e80176dc948dcbff5a4fc" type="ed25519">5f05a6c2d7f9b9f9a391ef6d6ea45baf813f1cead8aa647b6168b855d8300ed8e99c952142e08e8ac34ba5680e49ae1eabb98881f62429ec785f2057ff08b809</sig></incoming><entry><data><parent>00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000</parent><ref>6b4fb8cf-ae86-42c4-a51e-fe344a25c4ea</ref><serial>1</serial><date>2026-02-14</date><dateTimeRegistered>2026-02-14T09:09:31Z</dateTimeRegistered><description>Foobar</description><debit type="income"><unit>BTC</unit><account>general</account><amount>-13370000</amount></debit><credit type="asset"><unit>BTC</unit><account>general</account><amount>13370000</amount></credit><attachment mime="application/xml" uuid="b221358b-c6b8-433c-8ea2-14b5cb282f15"><digest algo="sha256">77473684a53bd344add4f55f66432e56955a134dda637e3021f4f2592ffe717b</digest><filename>test.xml</filename></attachment><attachment mime="text/plain" uuid="cb3f94e9-98d9-4f2a-9ff6-86e32ec02148"><digest algo="sha256">fb981668c18a279e285fc4d83fba1e836cc84dd4daa73c9697d3cfd2d8aca6e0</digest><filename>LICENSE</filename></attachment></data><sig type="ed25519" keyid="3b54648d60bb8a5b9e84fa0057f79b3a5996e511682e80176dc948dcbff5a4fc">f2a099adf8f5c8da17a4ddc0c3e65fc4f868da2c038691ff01311ce3018e7a65c7090fb2e312a283340b75f7c821134a3613c890c52f27c956a51010cf8d1b06</sig></entry><entry><data><parent>d2ec3d9132d40ce747a66049921fe864907e8ed8730d289a4c54bfcd6a9b8f3adcdead00f5c03cf4f5348fde609254461b16c78e1b482e05ff26281a58c76fc2</parent><ref>044e45ca-07b0-4496-bb32-61107f7c1796</ref><serial>2</serial><date>2026-02-14</date><dateTimeRegistered>2026-02-14T09:10:14Z</dateTimeRegistered><description>Barbarbar</description><debit type="expense"><unit>BTC</unit><account>luxury</account><amount>-421300</amount></debit><credit type="liability"><unit>BTC</unit><account>creditcard</account><amount>421300</amount></credit></data><sig type="ed25519" keyid="3b54648d60bb8a5b9e84fa0057f79b3a5996e511682e80176dc948dcbff5a4fc">425ae11c2d1808873c2da336a9c26386b6e45dd327f599186cca2766248bde62b38473e8a271119a6c382fa620f4c5a6de3471c2032824f76299360ef9e00009</sig></entry></ledger> +\ No newline at end of file diff --git a/dummy/tests/store.py b/dummy/tests/store.py @@ -104,5 +104,22 @@ class TestStore(unittest.TestCase): store.load(acl=acl) + def test_store_import(self): + fp = os.path.join(testdir, 'import.xml') + ledger = Ledger.from_file(fp) + store = LedgerStore(self.store, ledger) + store.put_all(store_assets=True) + + # TODO: less hacky test, perhaps a ledger.rewind() to get to zero state with everything else intact? + topic = ledger.topic + uidx = ledger.uidx + acl = ledger.acl + ledger = Ledger(uidx, topic=topic, acl=acl) + store = LedgerStore(self.store, ledger) + store.load(acl=acl) + # TODO: improve this test + self.assertEqual(len(ledger.entries), 2) + + if __name__ == '__main__': unittest.main() diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py @@ -487,7 +487,18 @@ class Entry: return entry + + """Verify signature on entry. + + At least one signature must be valid for one of the public keys in the wallet or ACL. + :param wallet: Wallet holding a public key to verify. + :type wallet: usawa.Wallet + :param acl: A collection of public keys to verify. + :type acl: usawa.ACL + :raises VerifyError: Invalid signature. + :raises ValueError: Neither wallet nor ACL supplied. + """ def verify(self, wallet=None, acl=None): if wallet == None and acl == None: raise ValueError('verify needs at least one of wallet or acl') diff --git a/dummy/usawa/ledger.py b/dummy/usawa/ledger.py @@ -140,6 +140,7 @@ class RunningTotal: return RunningTotal(unit, asset=asset, liability=liability) + """Generate an XML tree from the current state of the object. The XML generated can be used as a "real" or "virt" sub-element of the ledger/incoming/ element. @@ -566,6 +567,16 @@ class Ledger: return ledger.check() + @staticmethod + def from_file(filepath): + f = open(filepath, 'rb') + v = f.read() + f.close() + tree = lxml.etree.fromstring(v) + return Ledger.from_tree(tree) + + + """Append all entries from XML tree to ledger. :param tree: A parsed XML tree. diff --git a/dummy/usawa/runnable/add.py b/dummy/usawa/runnable/add.py @@ -29,6 +29,8 @@ class Context: self.output = None self.f = None self.attach = [] + self.valkey_host = None + self.valkey_port = None def close(self): @@ -72,6 +74,9 @@ class Context: o = Asset.from_file(v) ctx.attach.append(o) + ctx.valkey_host = args.valkey_host + ctx.valkey_port = args.valkey_port + return ctx @@ -129,6 +134,9 @@ argp.add_argument('-d', '--description', dest='description', type=str, help='int argp.add_argument('-u', '--unit', type=str, default=UnitIndex.default_unit, help='Unit to use for transaction') argp.add_argument('--unit-precision', dest='unit_precision', type=int, default=UnitIndex.default_precision, help='Unit precision') argp.add_argument('--unit-rate', dest='unit_precision', type=float, default=1.0, help='Unit exchange rate') +argp.add_argument('--valkey-host', dest='valkey_host', type=str, default='localhost', help='Valkey host') +argp.add_argument('--valkey-port', dest='valkey_port', type=int, default=6379, help='Valkey port') + argp.add_argument('ledger_xml_file', type=str, help='load ledger metadata from XML file') arg = argp.parse_args() ctx = Context.from_args(arg) @@ -140,7 +148,7 @@ ledger_tree = load(arg.ledger_xml_file) uidx = UnitIndex.from_tree(ledger_tree) ledger = Ledger.from_tree(ledger_tree) -db = ValkeyStore('') +db = ValkeyStore('', host=ctx.valkey_host, port=ctx.valkey_port) store = LedgerStore(db, ledger) pk = store.get_key() wallet = DemoWallet(privatekey=pk) diff --git a/dummy/usawa/runnable/import.py b/dummy/usawa/runnable/import.py @@ -0,0 +1,73 @@ +import os +import sys +import logging +import urllib.parse +import argparse +import uuid +import datetime + +from usawa import Ledger, Entry, EntryPart, DemoWallet, load, ACL +from usawa.constant import CATEGORIES +from usawa.store import LedgerStore +from whee.valkey import ValkeyStore + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + + +class Context: + + def __init__(self): + self.unit = None + self.uidx = None + self.output = None + self.f = None + self.valkey_host = None + self.valkey_port = None + + + def close(self): + if self.f and self.f != sys.stdout: + self.f.close() + + + def open(self, output): + if output == '<stdout>': + self.f = sys.stdout.buffer + logg.debug('output is stdout') + else: + self.f = open(output, 'wb') + return self + + @staticmethod + def from_args(args): + ctx = Context() + if args.output != None: + ctx.output = os.path.realpath(args.output) + else: + ctx.output = '<stdout>' + + ctx.valkey_host = args.valkey_host + ctx.valkey_port = args.valkey_port + + return ctx + + +argp = argparse.ArgumentParser() +argp.add_argument('-o', type=str, dest='output', help='output file for resulting XML document') +argp.add_argument('--valkey-host', dest='valkey_host', type=str, default='localhost', help='Valkey host') +argp.add_argument('--valkey-port', dest='valkey_port', type=int, default=6379, help='Valkey port') +argp.add_argument('ledger_xml_file', type=str, help='load ledger metadata from XML file') +arg = argp.parse_args() +ctx = Context.from_args(arg) + +ledger = Ledger.from_file(arg.ledger_xml_file) + +storedb = ValkeyStore('', host=ctx.valkey_host, port=ctx.valkey_port) +store = LedgerStore(storedb, ledger) +#pk = store.get_key() +#wallet = DemoWallet(privatekey=pk) +#acl = ACL.from_wallet(wallet) +#store.load(acl=acl) +store.put_all(store_assets=True) +sys.stdout.buffer.write(ledger.to_string()) diff --git a/dummy/usawa/runnable/view.py b/dummy/usawa/runnable/view.py @@ -22,6 +22,8 @@ class Context: self.uidx = None self.output = None self.f = None + self.valkey_host = None + self.valkey_port = None def close(self): @@ -44,11 +46,17 @@ class Context: ctx.output = os.path.realpath(args.output) else: ctx.output = '<stdout>' + + ctx.valkey_host = args.valkey_host + ctx.valkey_port = args.valkey_port + return ctx argp = argparse.ArgumentParser() argp.add_argument('-o', type=str, dest='output', help='output file for resulting XML document') +argp.add_argument('--valkey-host', dest='valkey_host', type=str, default='localhost', help='Valkey host') +argp.add_argument('--valkey-port', dest='valkey_port', type=int, default=6379, help='Valkey port') argp.add_argument('ledger_xml_file', type=str, help='load ledger metadata from XML file') arg = argp.parse_args() ctx = Context.from_args(arg) @@ -58,7 +66,7 @@ ledger_tree = load(arg.ledger_xml_file) uidx = UnitIndex.from_tree(ledger_tree) ledger = Ledger.from_tree(ledger_tree) -storedb = ValkeyStore('') +storedb = ValkeyStore('', host=ctx.valkey_host, port=ctx.valkey_port) store = LedgerStore(storedb, ledger) pk = store.get_key() wallet = DemoWallet(privatekey=pk) diff --git a/dummy/usawa/store.py b/dummy/usawa/store.py @@ -282,3 +282,25 @@ class LedgerStore(Interface): """ def get(self, k): return self.__o.get(k) + + + """Store all entries in the ledger state. + + Errors due to duplicate entry and asset insert attempts will be ignored. + + :param store_assets: Add all attachment assets from each entry. + :type store_assets: boolean + :raises FileExistsError: If duplicate entry is found. + """ + def put_all(self, store_assets=False): + for k in self.ledger.entries.keys(): + entry = self.ledger.entries[k] + try: + self.add_entry(entry, update_ledger=False) + except FileExistsError as e: + logg.info('putall skip duplicate entry {}'.format(entry)) + for asset in entry.attachment: + try: + self.add_asset(asset) + except FileExistsError: + logg.info('putall skip duplicate asset {}'.format(asset))