from os import environ
from pathlib import Path
from sys import exit as sys_exit
-from typing import Optional
+from typing import Optional, Self
from plomlib.web import PlomHttpHandler, PlomHttpServer, PlomQueryMap
PATH_TEMPLATES = Path('templates')
+class Wealth:
+ """Collects amounts mapped to currencies."""
+
+ def __init__(self, moneys: Optional[dict[str, Decimal]] = None) -> None:
+ self.moneys = moneys if moneys else {}
+
+ def _inc_by(self, other: Self, add=True) -> Self:
+ for currency, amount in other.moneys.items():
+ if currency not in self.moneys:
+ self.moneys[currency] = Decimal(0)
+ self.moneys[currency] += amount if add else -amount
+ return self
+
+ def __iadd__(self, other: Self) -> Self:
+ return self._inc_by(other, True)
+
+ def __isub__(self, other: Self) -> Self:
+ return self._inc_by(other, False)
+
+ @property
+ def sink_empty(self) -> bool:
+ """Return if all evens out to zero."""
+ return not bool(self.as_sink.moneys)
+
+ @property
+ def as_sink(self) -> 'Wealth':
+ """Drop zero amounts, invert non-zero ones."""
+ sink = Wealth()
+ for moneys in [Wealth({c: a}) for c, a in self.moneys.items() if a]:
+ sink = moneys
+ return sink
+
+
class DatLine:
"""Line of .dat file parsed into comments and machine-readable data."""
def __init__(self, booking_id: int, code: str) -> None:
super().__init__(booking_id)
- self.account, self.currency = '', ''
- self.amount: Optional[Decimal] = None
if not code[0].isspace():
- self.error = 'non-intro line not indented'
+ self.error = 'transfer line not indented'
return
toks = code.lstrip().split()
self.account = toks[0]
- if len(toks) not in {1, 3}:
- self.error = 'illegal number of tokens'
- return
- if 3 == len(toks):
+ self.amount: Optional[Decimal] = None
+ if 1 == len(toks):
+ self.currency = ''
+ elif 3 == len(toks):
self.currency = toks[2]
try:
self.amount = Decimal(toks[1])
except DecimalInvalidOperation:
self.error = 'improper amount value'
- return
+ else:
+ self.error = 'illegal number of tokens'
@property
def amount_short(self) -> str:
self.dat_lines[0].code)
for dat_line in self.dat_lines[1:]:
dat_line.booking_line = TransferLine(self.id_, dat_line.code)
+ self.account_changes: dict[str, Wealth] = {}
+ self.sink_account = None
+ for dat_line in [dl for dl in self.dat_lines if dl.error]:
+ return
+ changes = Wealth()
+ for transfer_line in [dl.booking_line for dl in self.dat_lines[1:]]:
+ assert isinstance(transfer_line, TransferLine)
+ if transfer_line.account not in self.account_changes:
+ self.account_changes[transfer_line.account] = Wealth()
+ if transfer_line.amount is None:
+ if self.sink_account:
+ transfer_line.error = 'second sink'
+ return
+ self.sink_account = transfer_line.account
+ continue
+ change = Wealth({transfer_line.currency: transfer_line.amount})
+ self.account_changes[transfer_line.account] += change
+ changes += change
+ if self.sink_account:
+ self.account_changes[self.sink_account] += changes.as_sink
+ elif not changes.sink_empty:
+ last_line = self.dat_lines[-1]
+ assert isinstance(last_line.booking_line, BookingLine)
+ last_line.booking_line.error = 'needed sink missing'
class Handler(PlomHttpHandler):