diff --git a/src/homebank/Homebank.py b/src/homebank/Homebank.py index 6e4b5a9..fc431fe 100644 --- a/src/homebank/Homebank.py +++ b/src/homebank/Homebank.py @@ -1,42 +1,141 @@ from __future__ import annotations -from typing import Optional, Dict, List -from lxml import etree + import datetime +from dataclasses import dataclass +from typing import Dict, List, Optional, Tuple + +from lxml import etree +# XML functions +def get_attribute(node: etree.Element, name: str) -> str: + if not name in node.attrib: + raise KeyError(f"Node '{node.tag}' in line {node.sourceline} is missing attribute '{name}'") + + return node.attrib[name] + +def get_attribute_optional(node: etree.Element, name: str) -> Optional[str]: + return node.attrib.get(name, None) + +def get_attribute_optional_int(node: etree.Element, name: str) -> Optional[int]: + if not name in node.attrib: return None + val = node.attrib[name] + try: + return int(val) + except ValueError as ex: + raise type(ex)(f"Could not convert attribute '{name}' of node '{node.tag}' in line {node.sourceline} to integer") from ex + +def get_attribute_int(node: etree.Element, name: str) -> int: + val = get_attribute_optional_int(node, name) + if val is None: + raise KeyError(f"Node '{node.tag}' in line {node.sourceline} is missing attribute '{name}'") + return val + +def get_attribute_optional_float(node: etree.Element, name: str) -> Optional[float]: + if not name in node.attrib: return None + val = node.attrib[name] + try: + return float(val) + except ValueError as ex: + raise type(ex)(f"Could not convert attribute '{name}' of node '{node.tag}' in line {node.sourceline} to float") from ex + +def get_attribute_float(node: etree.Element, name: str) -> float: + val = get_attribute_optional_float(node, name) + if val is None: + raise KeyError(f"Node '{node.tag}' in line {node.sourceline} is missing attribute '{name}'") + return val + + +# Homebank Data classes + +@dataclass class Payee: - def __init__(self, name: str): - self.name: str = name + name: str def __repr__(self) -> str: return f"Payee(\"{self.name}\")" + @staticmethod + def _from_node(node: etree.Element) -> Tuple[int, "Payee"]: + key = get_attribute_int(node, "key") + name = get_attribute(node, "name") + return key, Payee(name) + + +@dataclass class Account: - def __init__(self, name: str): - self.name: str = name + name: str def __repr__(self) -> str: return f"Account(\"{self.name}\")" + @staticmethod + def _from_node(node: etree.Element) -> Tuple[int, "Account"]: + key = get_attribute_int(node, "key") + name = get_attribute(node, "name") + return key, Account(name) + + +@dataclass class Category: - def __init__(self, name: str, parent: Optional[Category] = None): - self.name: str = name - self.parent: Optional[Category] = parent + name: str + parent: Optional[Category] = None def __repr__(self) -> str: if self.parent is not None: return f"Category(\"{self.name}\", {repr(self.parent)})" else: return f"Category(\"{self.name}\", None)" - + + @staticmethod + def _from_node(node: etree.Element, categories: Dict[int, "Category"]) -> Tuple[int, "Category"]: + key: int = get_attribute_int(node, "key") + name: str = get_attribute(node, "name") + parent: Optional[int] = get_attribute_optional_int(node, "parent") + parent_class: Optional["Category"] = None + if parent is not None: + if parent not in categories: + raise KeyError(f"Parent category {parent} for category '{name}' not found") + parent_class = categories[parent] + return key, Category(name, parent_class) + + +@dataclass class Operation: - def __init__(self, date: datetime.date, amount: float, account: Account, dst_account: Optional[Account], payee: Optional[Payee], category: Optional[Category], wording: Optional[str]): - self.date: datetime.date = date - self.amount: float = amount - self.account: Account = account - self.dst_account: Optional[Account] = dst_account - self.payee: Optional[Payee] = payee - self.category: Optional[Category] = category + date: datetime.date + amount: float + account: Account + dst_account: Optional[Account] + payee: Optional[Payee] + category: Optional[Category] + wording: Optional[str] + + @staticmethod + def _from_node(node: etree.Element, accounts: Dict[int, Account], payees: Dict[int, Payee], categories: Dict[int, Category]) -> "Operation": + date_i = get_attribute_int(node, "date") + date = datetime.date(1,1,1) + datetime.timedelta(days=date_i-1) + amount = get_attribute_float(node, "amount") + def find_account(account_i: int): + if not account_i in accounts: + raise KeyError(f"Account {account} for operation ({date}, {amount}) not found") + return accounts[account_i] + account_i = get_attribute_int(node, "account") + account = find_account(account_i) + dst_account_i = get_attribute_optional_int(node, "dst_account") + dst_account: Optional[Account] = None + if dst_account_i is not None: + dst_account = find_account(dst_account_i) + payee_i = get_attribute_optional_int(node, "payee") + payee: Optional[Payee] = None + if payee_i is not None: + payee = payees[payee_i] + category_i = get_attribute_optional_int(node, "category") + category: Optional[Category] = None + if category_i is not None: + category = categories[category_i] + wording = get_attribute_optional(node, "wording") + return Operation(date, amount, account, dst_account, payee, category, wording) + class Homebank: def __init__(self, path: str): @@ -49,20 +148,14 @@ class Homebank: for node in root: if node.tag == "account": - self.accounts[int(node.attrib["key"])] = Account(node.attrib["name"]) + key, account = Account._from_node(node) + self.accounts[key] = account elif node.tag == "pay": - self.payees[int(node.attrib["key"])] = Payee(node.attrib["name"]) + key, payee = Payee._from_node(node) + self.payees[key] = payee elif node.tag == "cat": - parent = node.attrib.get("parent", None) - if parent is not None: parent = self.categories[int(parent)] - self.categories[int(node.attrib["key"])] = Category(node.attrib["name"], parent) + key, category = Category._from_node(node, self.categories) + self.categories[key] = category elif node.tag == "ope": - dst_account = node.attrib.get("dst_account", None) - if dst_account is not None: dst_account = self.accounts[int(dst_account)] - payee = node.attrib.get("payee", None) - if payee is not None: payee = self.payees[int(payee)] - category = node.attrib.get("category", None) - if category is not None: category = self.categories[int(category)] - date = datetime.date(1,1,1) + datetime.timedelta(days=int(node.attrib["date"])-1) - self.operations.append(Operation(date, float(node.attrib["amount"]), self.accounts[int(node.attrib["account"])], dst_account, payee, category, node.attrib.get("wording", None))) - + operation = Operation._from_node(node, self.accounts, self.payees, self.categories) + self.operations.append(operation) diff --git a/src/homebank/__init__.py b/src/homebank/__init__.py index e69de29..15a1d6d 100644 --- a/src/homebank/__init__.py +++ b/src/homebank/__init__.py @@ -0,0 +1 @@ +from .Homebank import Account, Category, Homebank, Operation, Payee