Improve error messages when reading XML

This commit is contained in:
Jonny007-MKD 2023-03-20 00:52:16 +01:00
parent a5e07fd24d
commit 016f93ce65
2 changed files with 125 additions and 31 deletions

View file

@ -1,42 +1,141 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional, Dict, List
from lxml import etree
import datetime 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: class Payee:
def __init__(self, name: str): name: str
self.name: str = name
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Payee(\"{self.name}\")" 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: class Account:
def __init__(self, name: str): name: str
self.name: str = name
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Account(\"{self.name}\")" 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: class Category:
def __init__(self, name: str, parent: Optional[Category] = None): name: str
self.name: str = name parent: Optional[Category] = None
self.parent: Optional[Category] = parent
def __repr__(self) -> str: def __repr__(self) -> str:
if self.parent is not None: if self.parent is not None:
return f"Category(\"{self.name}\", {repr(self.parent)})" return f"Category(\"{self.name}\", {repr(self.parent)})"
else: else:
return f"Category(\"{self.name}\", None)" 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: 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]): date: datetime.date
self.date: datetime.date = date amount: float
self.amount: float = amount account: Account
self.account: Account = account dst_account: Optional[Account]
self.dst_account: Optional[Account] = dst_account payee: Optional[Payee]
self.payee: Optional[Payee] = payee category: Optional[Category]
self.category: Optional[Category] = 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: class Homebank:
def __init__(self, path: str): def __init__(self, path: str):
@ -49,20 +148,14 @@ class Homebank:
for node in root: for node in root:
if node.tag == "account": 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": 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": elif node.tag == "cat":
parent = node.attrib.get("parent", None) key, category = Category._from_node(node, self.categories)
if parent is not None: parent = self.categories[int(parent)] self.categories[key] = category
self.categories[int(node.attrib["key"])] = Category(node.attrib["name"], parent)
elif node.tag == "ope": elif node.tag == "ope":
dst_account = node.attrib.get("dst_account", None) operation = Operation._from_node(node, self.accounts, self.payees, self.categories)
if dst_account is not None: dst_account = self.accounts[int(dst_account)] self.operations.append(operation)
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)))

View file

@ -0,0 +1 @@
from .Homebank import Account, Category, Homebank, Operation, Payee