commit d2fc48a40122066dbcd7574eed482204d252b84e
parent b7ae7b3d91fda1296ba615bea6dfcd7888e3017f
Author: lash <dev@holbrook.no>
Date: Wed, 5 Nov 2025 22:49:01 +0000
WIP entry creation script
Diffstat:
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()