usawa

Signed, immutable accounting.
Info | Log | Files | Refs | Submodules | LICENSE

commit 186ddddd90a9e21f32f42786d7c0ee26b13859d7
parent a92686dcda65ba4f56a46eec4530e8a3371602e9
Author: lash <dev@holbrook.no>
Date:   Wed, 11 Mar 2026 07:57:31 -0600

Add account object to use for validation and in index

Diffstat:
Mdummy/tests/account.py | 21++++++++++++---------
Mdummy/usawa/account.py | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
2 files changed, 70 insertions(+), 17 deletions(-)

diff --git a/dummy/tests/account.py b/dummy/tests/account.py @@ -20,21 +20,24 @@ class TestAccount(unittest.TestCase): def test_account_lock(self): idx = AccountIndex(self.uidx) - idx.add('FOO', 'bar.baz') - idx.add('FOO', 'bar.baz') + # missing account type + with self.assertRaises(AttributeError): + idx.add('bar/baz', sym='FOO') + idx.add('liability/bar/baz', sym='FOO') + idx.add('liability/bar/baz', sym='FOO') with self.assertRaises(AccountError): - idx.add('FOO', 'bar.baz-') + idx.add('asset/bar/baz-', sym='FOO') with self.assertRaises(AccountError): - idx.add('BAZ', 'foo.bar') - self.assertFalse(idx.check('BAR', 'foo.baz')) - self.assertTrue(idx.check('FOO', 'bar.baz')) + idx.add('asset/foo/bar', sym='BAZ') + self.assertFalse(idx.check('BAR', 'liability/foo/baz')) + self.assertTrue(idx.check('FOO', 'liability/bar/baz')) def test_account_list(self): idx = AccountIndex(self.uidx) - idx.add('FOO', 'bar.bar') - idx.add('FOO', 'bar.baz') - idx.add('BAR', 'foo.baz') + idx.add('asset/bar/bar', sym='FOO') + idx.add('liability/bar/baz', sym='FOO') + idx.add('asset/foo/baz', sym='BAR') v = list(idx) logg.debug('results {}'.format(v)) self.assertEqual(len(v), 3) diff --git a/dummy/usawa/account.py b/dummy/usawa/account.py @@ -1,3 +1,4 @@ +import enum import logging from .error import AccountError @@ -5,32 +6,81 @@ from .error import AccountError logg = logging.getLogger('account') -def default_check(path): - parts = path.split('.') +def check_path_parts(path): + parts = path.split('/') for v in parts: if not v.isalnum(): raise AccountError('invalid part: ' + v) - return True + #return True + typ = getattr(AccountType, parts[0].lower()) + return (typ, parts,) + + +def from_account_path(p, sym=None, typ=None): + if sym != None: + p = sym + '.' + p + o = p.split('.') + logg.debug('have {} {} {}'.format(p, sym, typ)) + if len(o) != 2: + raise ValueError('account path should have zero or one symbol specifier') + sym = o[0] + o = check_path_parts(o[1]) + typ = o[0] + path = o[1] + + return (sym, typ, path,) + + +class AccountType(enum.Enum): + liability = 'Liability' + asset = 'Asset' + income = 'Income' + expense = 'Expense' + imprt = 'Import' + export = 'Export' + + +class Account: + + path_parser = from_account_path + + def __init__(self, sym, typ, segments): + if not isinstance(typ, AccountType): + raise ValueError('invalid account type') + self.sym = sym + self.typ = typ + self.segments = segments + + + @staticmethod + def from_path(path, sym=None, typ=None): + o = Account.path_parser(path, sym=sym, typ=typ) + return Account(o[0], o[1], o[2]) + + + def to_path(self): + path = self.segments.join('/') + path = '{}.{}/{}'.format(self.sym, self.typ, path) class AccountIndex: - def __init__(self, unitindex, pathvalidator=default_check): + def __init__(self, unitindex): #, pathvalidator=default_check): self.uidx = unitindex self.accounts = {} self.locked = False - self.validate = pathvalidator + #self.validate = pathvalidator self.iterval = None - def add(self, sym, path): + def add(self, path, sym=None, typ=None): + account = Account.from_path(path, sym=sym, typ=typ) try: - sym = self.uidx.sym(sym) + sym = self.uidx.sym(account.sym) except KeyError: raise AccountError('unknown unit ' + sym) if self.locked: raise AccountError('account index locked') - self.validate(path) if self.accounts.get(sym) == None: self.accounts[sym] = [] elif path in self.accounts[sym]: