more and better models
This commit is contained in:
+66
-3
@@ -2,18 +2,81 @@
|
||||
|
||||
from datetime import date
|
||||
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}$"
|
||||
|
||||
|
||||
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):
|
||||
id: int = Field(alias="Id1", description="internal B4w ID")
|
||||
account_iban: str = Field(alias="OwnrAcctIBAN", pattern=IBAN_PATTERN)
|
||||
account_currency: Currency = Field(alias="OwnrAcctCcy")
|
||||
date_valid: date = Field(alias="ValDt")
|
||||
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
|
||||
def from_dict(cls, d: dict) -> B4wExport:
|
||||
return cls.model_validate(d)
|
||||
def from_str(cls, s: str) -> Category | None:
|
||||
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
@@ -11,9 +11,7 @@ def reader_raw(path: Path):
|
||||
with path.open() as fid:
|
||||
return list(csv.DictReader(fid))
|
||||
|
||||
|
||||
def reader_b4w(path: Path):
|
||||
raw = reader_raw(path)
|
||||
return list(map(B4wExport.from_dict, raw))
|
||||
|
||||
|
||||
|
||||
return list(map(B4wExport.model_validate, raw))
|
||||
|
||||
Reference in New Issue
Block a user