Improve error messages when reading XML
This commit is contained in:
		
							parent
							
								
									a5e07fd24d
								
							
						
					
					
						commit
						016f93ce65
					
				
					 2 changed files with 125 additions and 31 deletions
				
			
		| 
						 | 
				
			
			@ -1,27 +1,85 @@
 | 
			
		|||
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:
 | 
			
		||||
| 
						 | 
				
			
			@ -29,14 +87,55 @@ class Category:
 | 
			
		|||
        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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
from .Homebank import Account, Category, Homebank, Operation, Payee
 | 
			
		||||
		Loading…
	
		Reference in a new issue