85 lines
2.3 KiB
Python
85 lines
2.3 KiB
Python
from decimal import Decimal
|
|
import datetime as dt
|
|
import re
|
|
from typing import List, Tuple, NamedTuple
|
|
|
|
from dateutil import parser
|
|
|
|
|
|
DATE_REGEX = r"[1-2][9|0]\d\d-\d\d-\d\d"
|
|
|
|
|
|
class ValidationError(Exception):
|
|
pass
|
|
|
|
|
|
class Transaction(NamedTuple):
|
|
date: dt.date
|
|
source: str
|
|
target: str
|
|
amount: Decimal
|
|
|
|
|
|
def ingest(transaction_strings: List[str]):
|
|
"""
|
|
Parse comma-separated strings into Transactions.
|
|
Build `entities` dictionary, with keys of entity names and values of
|
|
lists of tuples, 0-date 1-amount
|
|
"""
|
|
transactions = []
|
|
entity_names = set()
|
|
for transaction_string in transaction_strings:
|
|
date, source, target, amount = parse_and_validate_transaction_string(
|
|
transaction_string
|
|
)
|
|
transaction = Transaction(date, source, target, amount)
|
|
transactions.append(transaction)
|
|
entity_names.update((source, target))
|
|
|
|
global entities
|
|
entities = {entity_name: [] for entity_name in entity_names}
|
|
|
|
for t in sorted(transactions, key=lambda t: t.date):
|
|
entities[t.source].append((t.date, -t.amount))
|
|
entities[t.target].append((t.date, t.amount))
|
|
|
|
|
|
def _get_transactions():
|
|
"""for testing"""
|
|
return [item for sublist in list(entities.values()) for item in sublist]
|
|
|
|
|
|
def get_balance(entity_name: str, on_date: dt.date = None) -> Decimal:
|
|
"""
|
|
Get the balance _at closing_ of a given date. If no date is given,
|
|
all transactions are summed -> "current balance".
|
|
"""
|
|
return sum(
|
|
amount
|
|
for date, amount in entities[entity_name]
|
|
if not on_date or date <= on_date
|
|
)
|
|
|
|
|
|
def parse_and_validate_transaction_string(
|
|
transaction_string: str,
|
|
) -> Tuple[dt.date, str, str, Decimal]:
|
|
|
|
try:
|
|
date_string, source, target, amount_string = transaction_string.split(",")
|
|
except ValueError:
|
|
raise ValidationError(
|
|
f"Please provide a date, source, target and amount: {transaction_string}"
|
|
)
|
|
|
|
if not all((date_string, source, target, amount_string)):
|
|
raise ValidationError(f"All items must have a value: {transaction_string}")
|
|
|
|
if not re.match(DATE_REGEX, date_string):
|
|
raise ValidationError(f"date is invalid: {date_string}")
|
|
|
|
date = parser.parse(date_string).date()
|
|
amount = Decimal(amount_string)
|
|
|
|
return date, source, target, amount
|