more and better models
This commit is contained in:
+66
-3
@@ -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
@@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user