more and better models

This commit is contained in:
2026-09-02 22:55:48 +02:00
parent f758d7b2a5
commit afadfb0475
3 changed files with 68 additions and 7 deletions
View File
+66 -3
View File
@@ -2,18 +2,81 @@
from datetime import date from datetime import date
from decimal import Decimal from decimal import Decimal
from enum import Enum
from pydantic import BaseModel, Field from pydantic import BaseModel, Field, ValidationError, field_validator
IBAN_PATTERN = r"^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$" IBAN_PATTERN = r"^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$"
class Currency(str, Enum):
EUR = "EUR"
USD = "USD"
GBP = "GBP"
CHF = "CHF"
class AmountType(str, Enum):
DBIT = "DBIT"
CRDT = "CRDT"
@property
def sign(self) -> int:
return -1 if self == self.DBIT else +1
class B4wExport(BaseModel): class B4wExport(BaseModel):
id: int = Field(alias="Id1", description="internal B4w ID") id: int = Field(alias="Id1", description="internal B4w ID")
account_iban: str = Field(alias="OwnrAcctIBAN", pattern=IBAN_PATTERN) account_iban: str = Field(alias="OwnrAcctIBAN", pattern=IBAN_PATTERN)
account_currency: Currency = Field(alias="OwnrAcctCcy")
date_valid: date = Field(alias="ValDt") date_valid: date = Field(alias="ValDt")
amount: Decimal = Field(alias="Amt") amount: Decimal = Field(alias="Amt")
amount_currency: Currency = Field(alias="AmtCcy")
amount_type: AmountType = Field(alias="CdtDbtInd")
reference: str = Field(alias = "RmtInf")
other_name: str = Field(alias = "RmtdNm")
other_iban: str = Field(alias = "RmtdAcctIBAN")
category: Category | None = Field(alias = "Category")
notes: str = Field(alias = "Notes")
@property
def amount_signed(self) -> Decimal:
return self.amount * self.amount_type.sign
@field_validator("category", mode="before")
@classmethod
def category_str_is_parsable(cls, category_str: str) -> Category | None:
category = Category.from_str(category_str)
assert category
category_str_2 = category.to_str()
if not category_str == category_str_2:
raise ValidationError(f"Category string cannot be parsed: {category_str}")
return category
class Category(BaseModel):
name: str = Field(pattern=r"[^:\r\n]+")
parent: Category | None = None
@classmethod @classmethod
def from_dict(cls, d: dict) -> B4wExport: def from_str(cls, s: str) -> Category | None:
return cls.model_validate(d) if not s:
return
components = s.split(":")
name = components[-1]
rest = ":".join(components[:-1])
return cls(name=name, parent=cls.from_str(rest))
def to_str(self) -> str | None:
if not self.parent:
return self.name
else:
return f"{self.parent.to_str()}:{self.name}"
def __str__(self):
return self.to_str()
def __repr__(self):
return f"Category.from_str(\"{self.__str__()}\")"
+2 -4
View File
@@ -11,9 +11,7 @@ def reader_raw(path: Path):
with path.open() as fid: with path.open() as fid:
return list(csv.DictReader(fid)) return list(csv.DictReader(fid))
def reader_b4w(path: Path): def reader_b4w(path: Path):
raw = reader_raw(path) raw = reader_raw(path)
return list(map(B4wExport.from_dict, raw)) return list(map(B4wExport.model_validate, raw))