diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/models.py b/src/models.py index f4f6009..c8e1657 100644 --- a/src/models.py +++ b/src/models.py @@ -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__()}\")" diff --git a/src/reader.py b/src/reader.py index 39a3ae2..9eaaa18 100644 --- a/src/reader.py +++ b/src/reader.py @@ -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))