usawa

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

commit d2fc48a40122066dbcd7574eed482204d252b84e
parent b7ae7b3d91fda1296ba615bea6dfcd7888e3017f
Author: lash <dev@holbrook.no>
Date:   Wed,  5 Nov 2025 22:49:01 +0000

WIP entry creation script

Diffstat:
M.gitignore | 3+++
Adummy/create.py | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdummy/demo.py | 211+------------------------------------------------------------------------------
Mdummy/running.xml | 5+++--
Adummy/svcontas/__init__.py | 257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 328 insertions(+), 211 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1 +1,4 @@ *.html +__pycache__ +*.egg-info +.state diff --git a/dummy/create.py b/dummy/create.py @@ -0,0 +1,63 @@ +import argparse +import datetime + +import lxml.etree +import confini +import nacl.signing + +from svcontas import Ledger, Entry, get_units, init_ledger + + +seed = bytes.fromhex('2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae') +pk = nacl.signing.SigningKey(seed) +pubk = pk.verify_key + +state_serial = 0 +state_digest = b'00' * 64 + + +def save_state(): + f = open('.state', 'wb') + b = state_serial.to_bytes(8, byteorder='big') + f.write(b) + f.close() + return state_serial + + +def load_state(): + try: + f = open('.state', 'rb') + except FileNotFoundError: + return save_state() + b = f.read(8) + f.close() + state_serial = int.from_bytes(b, byteorder='big') + return state_serial + + +if __name__ == '__main__': + now = datetime.datetime.now() + argp = argparse.ArgumentParser() + argp.add_argument('amount', type=str, help='value amount in decimal or whole units') + argp.add_argument('-t', type=str, choices=['income', 'expense', 'asset', 'liability'], default='income', help='delta type') + argp.add_argument('-u', type=str, default='USD', help='unit of account') + argp.add_argument('-r', type=str, help='reference') + argp.add_argument('-p', type=str, help='parent') + argp.add_argument('-a', type=str, default='Miscellaneous', help='account') + argp.add_argument('--date', type=lambda d: datetime.date.fromisoformat(d), default=str(now.date()), help='date of transaction') + argp.add_argument('--xml-file', dest='xml_file', type=str, default='running.xml', help='xml file to manipulate') + arg = argp.parse_args() + + load_state() + tree = lxml.etree.parse(arg.xml_file) + root = tree.getroot() + units = get_units(root) + ledger = init_ledger(root, units) + + amount = units.from_floatstring(arg.u, arg.amount, allow_negative=False) + + state_serial += 1 + entry = Entry(arg.t, amount, arg.u, state_serial, arg.a, arg.date) + ledger.add_entry(entry) + + save_state() diff --git a/dummy/demo.py b/dummy/demo.py @@ -4,219 +4,12 @@ import datetime from lxml import etree +from svcontas import load, get_units, init_ledger + logging.basicConfig(level=logging.DEBUG) logg = logging.getLogger() -class NoopSigVerifier: - - def verify(self, msg, key, sig): - logg.warning('using noop verifier') - return True - - -class UnitIndex: - - def __init__(self, base): - self.base = base - self.detail = {} - self.exchange = {} - - - @staticmethod - def from_tree(tree): - r = UnitIndex(tree.get('base')) - for o in tree.iter('unit'): - r.detail[o.get('sym')] = int(o.find('precision').text) - r.exchange[o.get('sym')] = int(o.find('ex').text) - r.check() - return r - - - def check(self): - self.get(self.base) - return self - - - def get(self, k): - self.detail[k] - return k - - - def to_floatstring(self, sym, v): - c = self.detail[sym] - i = c * -1 - s = str(v) - l = len(s) - if l < c: - ss = '0' * c - s = '0' + ss[:c-l] + s - return s[:i] + '.' + s[i:] - - -class Entry: - - def __init__(self, typ, amount, unit, serial, account, dt, description=None, parent=None): - self.typ = typ - self.parent = parent - self.amount = amount - self.unit = unit - self.serial = serial - self.account = account - self.dt = dt - self.attachment = [] - self.sigs = {} - - - def attach(self, mime, algo, digest, description=None, slug=None): - self.attachment.append((mime, algo, digest, description, slug,)) - - - def add_signature(self, keyid, sigdata): - self.sigs[keyid] = sigdata - - - @staticmethod - def from_tree(tree, unitindex): - o = tree.find('data') - amount = int(o.find('amount').text) - unit = unitindex.get(o.find('unit').text) - serial = int(o.find('serial').text) - account = o.find('account').text - dt = datetime.date.fromisoformat(o.find('date').text) - r = Entry(tree.get('type'), amount, unit, serial, account, dt) - return r - - -class RunningTotal: - - def __init__(self, sym, unitindex, asset=0, liability=0, income=0, expense=0): - self.sym = sym - self.asset = asset - self.liability = liability - self.income = income - self.expense = expense - self.unitindex = unitindex - - - def get_balance(self): - return self.asset - self.liability - - - def get_result(self): - return self.income - self.expense - - - def income_delta(self, v): - self.income += v - self.asset += v - - - def expense_delta(self, v): - self.expense += v - self.asset -= v - - - def asset_delta(self, v): - self.asset += v - - - def liability_delta(self, v): - self.liability += v - - - def apply_entry(self, entry): - fn = getattr(self, entry.typ + '_delta') - fn(entry.amount) - logg.debug('applied entry {} typ {} amount {} total {} balance {}'.format(entry.serial, entry.typ, entry.amount, getattr(self, entry.typ), self.unitindex.to_floatstring(self.sym, self.get_balance()))) - - - def __str__(self): - return 'running total {}: income {} expense {} asset {} liability {}'.format(self.sym, self.income, self.expense, self.asset, self.liability) - - -class Ledger: - - def __init__(self, base, unitindex, verifier=None): - self.uidx = unitindex - self.base = bytes.fromhex(base) - self.sigs = {} - if verifier == None: - verifier = NoopSigVerifier() - self.verifier = verifier - self.entries = {} - self.running = {} - - - def add_entry_from_tree(self, tree): - o = tree.find('data/parent') - - - def add_signature(self, sigdata, identity): - self.verifier.verify(self.base, identity, sigdata) - self.sigs[identity] = sigdata - logg.debug('add sig from key{}: {}'.format(identity, sigdata)) - - - @staticmethod - def from_tree(tree, unitindex, verifier=None): - o = tree.get('digest') - r = Ledger(o, unitindex, verifier=verifier) - for sig in tree.iter('sig'): - keyid = sig.get('keyid') - digest = sig.text - r.add_signature(digest, keyid) - - o = tree.find('real') - asset = int(o.find('asset').text) - liability = int(o.find('liability').text) - r.real = RunningTotal('.', unitindex, asset=asset, liability=liability) - logg.debug(r.real) - - for v in tree.iter('virt'): - income = int(v.find('income').text) - expense = int(v.find('expense').text) - asset = int(v.find('asset').text) - liability = int(v.find('liability').text) - sym = v.get('symbol') - r.running[sym] = RunningTotal(sym, unitindex, income=income, expense=expense, asset=asset, liability=liability) - logg.debug(r.running[sym]) - - return r.check() - - - def apply_tree(self, tree): - for v in tree.iter('entry'): - o = Entry.from_tree(v, self.uidx) - self.entries[o.serial] = o - self.running[o.unit].apply_entry(o) - - - def check(self): - return self - - - def __str__(self): - return "state: " + self.base.hex() - - -def init_ledger(tree, units): - o = tree.find('incoming') - return Ledger.from_tree(o, units) - - -def get_units(tree): - o = tree.find('units') - return UnitIndex.from_tree(o) - - -def load(fp): - f = open(fp, 'r') - tree = etree.parse(f) - f.close() - return tree.getroot() - - if __name__ == '__main__': fp = 'running.xml' try: diff --git a/dummy/running.xml b/dummy/running.xml @@ -16,7 +16,8 @@ <resolver algo="sha256" proto="https">g33k.holbrook.no</resolver> <identity keyid="f1d2d2f924e986ac86fdf7b36c94bcdf32beec15" didtype="web">nondominium.org/lash/</identity> <identity keyid="f1d2d2f924e986ac86fdf7b36c94bcdf32beec15" didtype="web">holbrook.no</identity> - <incoming serial="231" digest="b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"> + <incoming serial="231"> + <digest algo="sha512">b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944cb5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c</digest> <sig keyid="58851ae2166b3f1454193e0a7e821402dd1c1a91">1f74a9c0196a025f27d4e940c4abedfa2d37504f268aea359659cb65b85bd4d7974369507950006964c93391d3d4580ab8064f6d30a62908468ef771be952e95</sig> <sig keyid="566c38287d3f31c7e50836cae58e426c6bccc52d">117a57c72ed210b91469307a1c2e73fe2d5ee306cd8ccf1a9db4ecb15d38ecbbfc97d62fec4ab8aadb08c531f2d1ede34cb6e4d3987bcba63322a0767e532e13</sig> <real unit="BTC"> @@ -49,7 +50,7 @@ <attachment mime="image/jpeg"> <slug>troll</slug> <description>The face of a troll</description> - <digest algo="sha256">777b30c8fc40aea3c717777831a05c9f29c7b6735f1573e9b0b55373c264f6f3</digest> + <digest algo="sha512">777b30c8fc40aea3c717777831a05c9f29c7b6735f1573e9b0b55373c264f6f3777b30c8fc40aea3c717777831a05c9f29c7b6735f1573e9b0b55373c264f6f3</digest> </attachment> </data> <sig keyid="e242ed3bffccdf271b7fbaf34ed72d089537b42f">0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6/</sig> diff --git a/dummy/svcontas/__init__.py b/dummy/svcontas/__init__.py @@ -0,0 +1,257 @@ +import sys +import logging +import datetime + +from lxml import etree + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + + +class NoopSigVerifier: + + def verify(self, msg, key, sig): + logg.warning('using noop verifier') + return True + + +class UnitIndex: + + def __init__(self, base): + self.base = base + self.detail = {} + self.exchange = {} + + + @staticmethod + def from_tree(tree): + r = UnitIndex(tree.get('base')) + for o in tree.iter('unit'): + r.detail[o.get('sym')] = int(o.find('precision').text) + r.exchange[o.get('sym')] = int(o.find('ex').text) + r.check() + return r + + + def check(self): + self.get(self.base) + return self + + + def get(self, k): + self.detail[k] + return k + + + def to_floatstring(self, sym, v, allow_negative=True): + neg = v < 0 + if neg and not allow_negative: + raise ValueError('negative value not allowed') + v = abs(v) + c = self.detail[sym] + i = c * -1 + s = str(v) + l = len(s) + if l < c: + ss = '0' * c + s = '0' + ss[:c-l] + s + r = s[:i] + '.' + s[i:] + if neg: + r = '-' + r + return r + + + def from_floatstring(self, sym, v, allow_negative=True): + neg = False + if v[0] == '-': + if not allow_negative: + raise ValueError('negative value not allowed') + neg = True + v = v[1:] + c = self.detail[sym] + s = v.split('.') + if len(s) == 1: + return int(s[0]) * (10**c) + r = s[1] + l = len(r) + if l < c: + r += '0' * (c - l) + r = s[0] + r + if neg: + r *= -1 + return int(r) + + +class Entry: + + def __init__(self, typ, amount, unit, serial, account, tx_date, description=None, parent=None): + self.typ = typ + self.parent = parent + self.amount = amount + self.unit = unit + self.serial = serial + self.account = account + self.dt = tx_date + self.dtreg = datetime.datetime.now() + self.attachment = [] + self.sigs = {} + + + def attach(self, mime, algo, digest, description=None, slug=None): + self.attachment.append((mime, algo, digest, description, slug,)) + + + def add_signature(self, keyid, sigdata): + self.sigs[keyid] = sigdata + + + @staticmethod + def from_tree(tree, unitindex): + o = tree.find('data') + amount = int(o.find('amount').text) + unit = unitindex.get(o.find('unit').text) + serial = int(o.find('serial').text) + account = o.find('account').text + dt = datetime.date.fromisoformat(o.find('date').text) + r = Entry(tree.get('type'), amount, unit, serial, account, dt) + return r + + +class RunningTotal: + + def __init__(self, sym, unitindex, asset=0, liability=0, income=0, expense=0): + self.sym = sym + self.asset = asset + self.liability = liability + self.income = income + self.expense = expense + self.unitindex = unitindex + + + def get_balance(self): + return self.asset - self.liability + + + def get_result(self): + return self.income - self.expense + + + def income_delta(self, v): + self.income += v + self.asset += v + + + def expense_delta(self, v): + self.expense += v + self.asset -= v + + + def asset_delta(self, v): + self.asset += v + + + def liability_delta(self, v): + self.liability += v + + + def apply_entry(self, entry): + fn = getattr(self, entry.typ + '_delta') + fn(entry.amount) + logg.debug('applied entry {} typ {} amount {} total {} balance {}'.format(entry.serial, entry.typ, entry.amount, getattr(self, entry.typ), self.unitindex.to_floatstring(self.sym, self.get_balance()))) + + + def __str__(self): + return 'running total {}: income {} expense {} asset {} liability {}'.format(self.sym, self.income, self.expense, self.asset, self.liability) + + +class Ledger: + + def __init__(self, base, unitindex, verifier=None, tree=None): + self.uidx = unitindex + self.base = bytes.fromhex(base) + self.sigs = {} + if verifier == None: + verifier = NoopSigVerifier() + self.verifier = verifier + self.entries = {} + self.running = {} + self.tree = tree + + + def add_entry_from_tree(self, tree): + o = tree.find('data/parent') + + + def add_entry(self, entry): + self.running[entry.unit].apply_entry(entry) + try: + entries = self.entries[entry.serial] + except KeyError: + self.entries[entry.serial] = [] + entries = self.entries[entry.serial] + self.entries[entry.serial].append(entry) + + + def add_signature(self, sigdata, identity): + self.verifier.verify(self.base, identity, sigdata) + self.sigs[identity] = sigdata + logg.debug('add sig from key{}: {}'.format(identity, sigdata)) + + + @staticmethod + def from_tree(tree, unitindex, verifier=None): + o = tree.find('digest').text # verify that is sha512 + r = Ledger(o, unitindex, verifier=verifier, tree=tree) + for sig in tree.iter('sig'): + keyid = sig.get('keyid') + digest = sig.text + r.add_signature(digest, keyid) + + o = tree.find('real') + asset = int(o.find('asset').text) + liability = int(o.find('liability').text) + r.real = RunningTotal('.', unitindex, asset=asset, liability=liability) + logg.debug(r.real) + + for v in tree.iter('virt'): + income = int(v.find('income').text) + expense = int(v.find('expense').text) + asset = int(v.find('asset').text) + liability = int(v.find('liability').text) + sym = v.get('symbol') + r.running[sym] = RunningTotal(sym, unitindex, income=income, expense=expense, asset=asset, liability=liability) + logg.debug(r.running[sym]) + + return r.check() + + + def apply_tree(self, tree): + for v in tree.iter('entry'): + o = Entry.from_tree(v, self.uidx) + self.entries[o.serial] = o + self.running[o.unit].apply_entry(o) + + + def check(self): + return self + + + def __str__(self): + return "state: " + self.base.hex() + + +def init_ledger(tree, units): + o = tree.find('incoming') + return Ledger.from_tree(o, units) + + +def get_units(tree): + o = tree.find('units') + return UnitIndex.from_tree(o) + + +def load(fp): + f = open(fp, 'r') + tree = etree.parse(f) + f.close() + return tree.getroot()