back_populates option on category relationship
Due to the use of the dataclasses mixin on the SQLAlchemy types, a back_populates creates a RecursiveError when comparing two types. This occurs because the dataclass will overwrite the __eq__ operator, and it doesn't know when to stop comparing relationships. Removing the dataclasses isn't the best approach, since then __init__, __eq__ and __repr__ methods would have to be added to all types. Thus the solution was to remove the relationship on the child (on a one-to-one relationship) from the __eq__ operation, with the use of the compare parameter. Took the opportunity to define more logical __init__ methods on the `Rule` and child classes. Also revised the parameter options on some DB types.
This commit is contained in:
parent
bdd7cac4be
commit
01df97ed46
@ -163,14 +163,14 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
params = [
|
params = [
|
||||||
type.CategoryRule(
|
type.CategoryRule(
|
||||||
args["start"][0] if args["start"] else None,
|
|
||||||
args["end"][0] if args["end"] else None,
|
|
||||||
args["description"][0] if args["description"] else None,
|
|
||||||
args["regex"][0] if args["regex"] else None,
|
|
||||||
args["bank"][0] if args["bank"] else None,
|
|
||||||
args["min"][0] if args["min"] else None,
|
|
||||||
args["max"][0] if args["max"] else None,
|
|
||||||
cat,
|
cat,
|
||||||
|
start=args["start"][0] if args["start"] else None,
|
||||||
|
end=args["end"][0] if args["end"] else None,
|
||||||
|
description=args["description"][0] if args["description"] else None,
|
||||||
|
regex=args["regex"][0] if args["regex"] else None,
|
||||||
|
bank=args["bank"][0] if args["bank"] else None,
|
||||||
|
min=args["min"][0] if args["min"] else None,
|
||||||
|
max=args["max"][0] if args["max"] else None,
|
||||||
)
|
)
|
||||||
for cat in args["category"]
|
for cat in args["category"]
|
||||||
]
|
]
|
||||||
@ -215,14 +215,14 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
params = [
|
params = [
|
||||||
type.TagRule(
|
type.TagRule(
|
||||||
args["start"][0] if args["start"] else None,
|
|
||||||
args["end"][0] if args["end"] else None,
|
|
||||||
args["description"][0] if args["description"] else None,
|
|
||||||
args["regex"][0] if args["regex"] else None,
|
|
||||||
args["bank"][0] if args["bank"] else None,
|
|
||||||
args["min"][0] if args["min"] else None,
|
|
||||||
args["max"][0] if args["max"] else None,
|
|
||||||
tag,
|
tag,
|
||||||
|
start=args["start"][0] if args["start"] else None,
|
||||||
|
end=args["end"][0] if args["end"] else None,
|
||||||
|
description=args["description"][0] if args["description"] else None,
|
||||||
|
regex=args["regex"][0] if args["regex"] else None,
|
||||||
|
bank=args["bank"][0] if args["bank"] else None,
|
||||||
|
min=args["min"][0] if args["min"] else None,
|
||||||
|
max=args["max"][0] if args["max"] else None,
|
||||||
)
|
)
|
||||||
for tag in args["tag"]
|
for tag in args["tag"]
|
||||||
]
|
]
|
||||||
|
|||||||
@ -65,7 +65,7 @@ class Bank(Base, Export):
|
|||||||
BIC: Mapped[str] = mapped_column(String(8))
|
BIC: Mapped[str] = mapped_column(String(8))
|
||||||
type: Mapped[accounttype]
|
type: Mapped[accounttype]
|
||||||
|
|
||||||
nordigen: Mapped[Optional[Nordigen]] = relationship(lazy="joined", init=False)
|
nordigen: Mapped[Optional[Nordigen]] = relationship(init=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def format(self) -> dict[str, Any]:
|
def format(self) -> dict[str, Any]:
|
||||||
@ -98,16 +98,17 @@ class Transaction(Base, Export):
|
|||||||
description: Mapped[Optional[str]]
|
description: Mapped[Optional[str]]
|
||||||
amount: Mapped[money]
|
amount: Mapped[money]
|
||||||
|
|
||||||
split: Mapped[bool] = mapped_column(init=False, default=False)
|
split: Mapped[bool] = mapped_column(default=False)
|
||||||
|
|
||||||
|
category: Mapped[Optional[TransactionCategory]] = relationship(
|
||||||
|
back_populates="transaction", default=None
|
||||||
|
)
|
||||||
|
tags: Mapped[set[TransactionTag]] = relationship(default_factory=set)
|
||||||
|
note: Mapped[Optional[Note]] = relationship(
|
||||||
|
cascade="all, delete-orphan", passive_deletes=True, default=None
|
||||||
|
)
|
||||||
|
|
||||||
type: Mapped[str] = mapped_column(init=False)
|
type: Mapped[str] = mapped_column(init=False)
|
||||||
|
|
||||||
category: Mapped[Optional[TransactionCategory]] = relationship(init=False)
|
|
||||||
note: Mapped[Optional[Note]] = relationship(
|
|
||||||
cascade="all, delete-orphan", init=False, passive_deletes=True
|
|
||||||
)
|
|
||||||
tags: Mapped[set[TransactionTag]] = relationship(init=False)
|
|
||||||
|
|
||||||
__mapper_args__ = {"polymorphic_on": "type", "polymorphic_identity": "transaction"}
|
__mapper_args__ = {"polymorphic_on": "type", "polymorphic_identity": "transaction"}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -134,7 +135,7 @@ idfk = Annotated[
|
|||||||
|
|
||||||
|
|
||||||
class BankTransaction(Transaction):
|
class BankTransaction(Transaction):
|
||||||
bank: Mapped[bankfk] = mapped_column(nullable=True)
|
bank: Mapped[Optional[bankfk]] = mapped_column(default=None)
|
||||||
|
|
||||||
__mapper_args__ = {"polymorphic_identity": "bank", "polymorphic_load": "inline"}
|
__mapper_args__ = {"polymorphic_identity": "bank", "polymorphic_load": "inline"}
|
||||||
|
|
||||||
@ -148,7 +149,7 @@ class MoneyTransaction(Transaction):
|
|||||||
|
|
||||||
|
|
||||||
class SplitTransaction(Transaction):
|
class SplitTransaction(Transaction):
|
||||||
original: Mapped[idfk] = mapped_column(nullable=True)
|
original: Mapped[Optional[idfk]] = mapped_column(default=None)
|
||||||
|
|
||||||
__mapper_args__ = {"polymorphic_identity": "split", "polymorphic_load": "inline"}
|
__mapper_args__ = {"polymorphic_identity": "split", "polymorphic_load": "inline"}
|
||||||
|
|
||||||
@ -204,6 +205,15 @@ catfk = Annotated[
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Selector_T(enum.Enum):
|
||||||
|
unknown = enum.auto()
|
||||||
|
nullifier = enum.auto()
|
||||||
|
vacations = enum.auto()
|
||||||
|
rules = enum.auto()
|
||||||
|
algorithm = enum.auto()
|
||||||
|
manual = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
class TransactionCategory(Base, Export):
|
class TransactionCategory(Base, Export):
|
||||||
__tablename__ = "transactions_categorized"
|
__tablename__ = "transactions_categorized"
|
||||||
|
|
||||||
@ -211,7 +221,11 @@ class TransactionCategory(Base, Export):
|
|||||||
name: Mapped[catfk]
|
name: Mapped[catfk]
|
||||||
|
|
||||||
selector: Mapped[CategorySelector] = relationship(
|
selector: Mapped[CategorySelector] = relationship(
|
||||||
cascade="all, delete-orphan", lazy="joined"
|
cascade="all, delete-orphan", default=Selector_T.unknown
|
||||||
|
)
|
||||||
|
|
||||||
|
transaction: Mapped[Transaction] = relationship(
|
||||||
|
back_populates="category", init=False, compare=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -234,7 +248,7 @@ class Nordigen(Base, Export):
|
|||||||
name: Mapped[bankfk] = mapped_column(primary_key=True)
|
name: Mapped[bankfk] = mapped_column(primary_key=True)
|
||||||
bank_id: Mapped[Optional[str]]
|
bank_id: Mapped[Optional[str]]
|
||||||
requisition_id: Mapped[Optional[str]]
|
requisition_id: Mapped[Optional[str]]
|
||||||
invert: Mapped[Optional[bool]]
|
invert: Mapped[Optional[bool]] = mapped_column(default=None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def format(self) -> dict[str, Any]:
|
def format(self) -> dict[str, Any]:
|
||||||
@ -270,18 +284,9 @@ class TransactionTag(Base, Export):
|
|||||||
return hash(self.id)
|
return hash(self.id)
|
||||||
|
|
||||||
|
|
||||||
class Selector_T(enum.Enum):
|
|
||||||
unknown = enum.auto()
|
|
||||||
nullifier = enum.auto()
|
|
||||||
vacations = enum.auto()
|
|
||||||
rules = enum.auto()
|
|
||||||
algorithm = enum.auto()
|
|
||||||
manual = enum.auto()
|
|
||||||
|
|
||||||
|
|
||||||
categoryselector = Annotated[
|
categoryselector = Annotated[
|
||||||
Selector_T,
|
Selector_T,
|
||||||
mapped_column(Enum(Selector_T, inherit_schema=True), default=Selector_T.unknown),
|
mapped_column(Enum(Selector_T, inherit_schema=True)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -294,7 +299,7 @@ class CategorySelector(Base, Export):
|
|||||||
primary_key=True,
|
primary_key=True,
|
||||||
init=False,
|
init=False,
|
||||||
)
|
)
|
||||||
selector: Mapped[categoryselector]
|
selector: Mapped[categoryselector] = mapped_column(default=Selector_T.unknown)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def format(self):
|
def format(self):
|
||||||
@ -336,7 +341,7 @@ class Link(Base):
|
|||||||
link: Mapped[idfk] = mapped_column(primary_key=True)
|
link: Mapped[idfk] = mapped_column(primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
class Rule(Base, Export):
|
class Rule(Base, Export, init=False):
|
||||||
__tablename__ = "rules"
|
__tablename__ = "rules"
|
||||||
|
|
||||||
id: Mapped[idpk] = mapped_column(init=False)
|
id: Mapped[idpk] = mapped_column(init=False)
|
||||||
@ -355,6 +360,10 @@ class Rule(Base, Export):
|
|||||||
"polymorphic_on": "type",
|
"polymorphic_on": "type",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
def matches(self, t: BankTransaction) -> bool:
|
def matches(self, t: BankTransaction) -> bool:
|
||||||
valid = None
|
valid = None
|
||||||
if self.regex:
|
if self.regex:
|
||||||
@ -415,6 +424,10 @@ class CategoryRule(Rule):
|
|||||||
def format(self) -> dict[str, Any]:
|
def format(self) -> dict[str, Any]:
|
||||||
return super().format | dict(name=self.name)
|
return super().format | dict(name=self.name)
|
||||||
|
|
||||||
|
def __init__(self, name: str, **kwargs: Any) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.name = name
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(self.id)
|
return hash(self.id)
|
||||||
|
|
||||||
@ -438,5 +451,9 @@ class TagRule(Rule):
|
|||||||
def format(self) -> dict[str, Any]:
|
def format(self) -> dict[str, Any]:
|
||||||
return super().format | dict(tag=self.tag)
|
return super().format | dict(tag=self.tag)
|
||||||
|
|
||||||
|
def __init__(self, name: str, **kwargs: Any) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.tag = name
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(self.id)
|
return hash(self.id)
|
||||||
|
|||||||
@ -2,14 +2,11 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from pfbudget.db.model import Category, CategoryRule, Tag, TagRule
|
from pfbudget.db.model import Category, CategoryRule, Tag, TagRule
|
||||||
|
|
||||||
category_null = Category("null", None, set())
|
category_null = Category("null")
|
||||||
|
|
||||||
category1 = Category(
|
category1 = Category(
|
||||||
"cat#1",
|
"cat#1",
|
||||||
None,
|
rules={CategoryRule("cat#1", description="desc#1", max=Decimal(0))},
|
||||||
{CategoryRule(None, None, "desc#1", None, None, None, Decimal(0), "cat#1")},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
tag_1 = Tag(
|
tag_1 = Tag("tag#1", rules={TagRule("tag#1", description="desc#1", max=Decimal(0))})
|
||||||
"tag#1", {TagRule(None, None, "desc#1", None, None, None, Decimal(0), "tag#1")}
|
|
||||||
)
|
|
||||||
|
|||||||
@ -39,12 +39,16 @@ def banks(client: Client) -> list[Bank]:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def transactions(client: Client) -> list[Transaction]:
|
def transactions(client: Client) -> list[Transaction]:
|
||||||
transactions = [
|
transactions = [
|
||||||
Transaction(date(2023, 1, 1), "", Decimal("-10")),
|
Transaction(
|
||||||
|
date(2023, 1, 1),
|
||||||
|
"",
|
||||||
|
Decimal("-10"),
|
||||||
|
category=TransactionCategory(
|
||||||
|
"category", CategorySelector(Selector_T.algorithm)
|
||||||
|
),
|
||||||
|
),
|
||||||
Transaction(date(2023, 1, 2), "", Decimal("-50")),
|
Transaction(date(2023, 1, 2), "", Decimal("-50")),
|
||||||
]
|
]
|
||||||
transactions[0].category = TransactionCategory(
|
|
||||||
"name", CategorySelector(Selector_T.algorithm)
|
|
||||||
)
|
|
||||||
|
|
||||||
client.insert(transactions)
|
client.insert(transactions)
|
||||||
for i, transaction in enumerate(transactions):
|
for i, transaction in enumerate(transactions):
|
||||||
|
|||||||
@ -31,8 +31,8 @@ class TestDatabaseLoad:
|
|||||||
|
|
||||||
def test_insert(self, loader: Loader):
|
def test_insert(self, loader: Loader):
|
||||||
transactions = [
|
transactions = [
|
||||||
BankTransaction(date(2023, 1, 1), "", Decimal("-500"), "Bank#1"),
|
BankTransaction(date(2023, 1, 1), "", Decimal("-500"), bank="Bank#1"),
|
||||||
BankTransaction(date(2023, 1, 2), "", Decimal("500"), "Bank#2"),
|
BankTransaction(date(2023, 1, 2), "", Decimal("500"), bank="Bank#2"),
|
||||||
]
|
]
|
||||||
|
|
||||||
loader.load(transactions)
|
loader.load(transactions)
|
||||||
|
|||||||
@ -91,9 +91,9 @@ class TestExtractPSD2:
|
|||||||
def test_extract(self, extractor: Extractor, bank: Bank):
|
def test_extract(self, extractor: Extractor, bank: Bank):
|
||||||
assert extractor.extract(bank) == [
|
assert extractor.extract(bank) == [
|
||||||
BankTransaction(
|
BankTransaction(
|
||||||
dt.date(2023, 1, 14), "string", Decimal("328.18"), "Bank#1"
|
dt.date(2023, 1, 14), "string", Decimal("328.18"), bank="Bank#1"
|
||||||
),
|
),
|
||||||
BankTransaction(
|
BankTransaction(
|
||||||
dt.date(2023, 2, 14), "string", Decimal("947.26"), "Bank#1"
|
dt.date(2023, 2, 14), "string", Decimal("947.26"), bank="Bank#1"
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -20,8 +20,8 @@ from pfbudget.transform.transform import Transformer
|
|||||||
class TestTransform:
|
class TestTransform:
|
||||||
def test_nullifier(self):
|
def test_nullifier(self):
|
||||||
transactions = [
|
transactions = [
|
||||||
BankTransaction(date(2023, 1, 1), "", Decimal("-500"), "Bank#1"),
|
BankTransaction(date(2023, 1, 1), "", Decimal("-500"), bank="Bank#1"),
|
||||||
BankTransaction(date(2023, 1, 2), "", Decimal("500"), "Bank#2"),
|
BankTransaction(date(2023, 1, 2), "", Decimal("500"), bank="Bank#2"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for t in transactions:
|
for t in transactions:
|
||||||
@ -37,8 +37,8 @@ class TestTransform:
|
|||||||
|
|
||||||
def test_nullifier_inplace(self):
|
def test_nullifier_inplace(self):
|
||||||
transactions = [
|
transactions = [
|
||||||
BankTransaction(date(2023, 1, 1), "", Decimal("-500"), "Bank#1"),
|
BankTransaction(date(2023, 1, 1), "", Decimal("-500"), bank="Bank#1"),
|
||||||
BankTransaction(date(2023, 1, 2), "", Decimal("500"), "Bank#2"),
|
BankTransaction(date(2023, 1, 2), "", Decimal("500"), bank="Bank#2"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for t in transactions:
|
for t in transactions:
|
||||||
@ -54,14 +54,14 @@ class TestTransform:
|
|||||||
|
|
||||||
def test_nullifier_with_rules(self):
|
def test_nullifier_with_rules(self):
|
||||||
transactions = [
|
transactions = [
|
||||||
BankTransaction(date(2023, 1, 1), "", Decimal("-500"), "Bank#1"),
|
BankTransaction(date(2023, 1, 1), "", Decimal("-500"), bank="Bank#1"),
|
||||||
BankTransaction(date(2023, 1, 2), "", Decimal("500"), "Bank#2"),
|
BankTransaction(date(2023, 1, 2), "", Decimal("500"), bank="Bank#2"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for t in transactions:
|
for t in transactions:
|
||||||
assert not t.category
|
assert not t.category
|
||||||
|
|
||||||
rules = [CategoryRule(None, None, None, None, "Bank#1", None, None, "null")]
|
rules = [CategoryRule("null", bank="Bank#1")]
|
||||||
|
|
||||||
categorizer: Transformer = Nullifier(rules)
|
categorizer: Transformer = Nullifier(rules)
|
||||||
transactions = categorizer.transform(transactions)
|
transactions = categorizer.transform(transactions)
|
||||||
@ -69,7 +69,7 @@ class TestTransform:
|
|||||||
for t in transactions:
|
for t in transactions:
|
||||||
assert not t.category
|
assert not t.category
|
||||||
|
|
||||||
rules.append(CategoryRule(None, None, None, None, "Bank#2", None, None, "null"))
|
rules.append(CategoryRule("null", bank="Bank#2"))
|
||||||
categorizer = Nullifier(rules)
|
categorizer = Nullifier(rules)
|
||||||
transactions = categorizer.transform(transactions)
|
transactions = categorizer.transform(transactions)
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ class TestTransform:
|
|||||||
|
|
||||||
def test_tagger(self):
|
def test_tagger(self):
|
||||||
transactions = [
|
transactions = [
|
||||||
BankTransaction(date(2023, 1, 1), "desc#1", Decimal("-10"), "Bank#1")
|
BankTransaction(date(2023, 1, 1), "desc#1", Decimal("-10"), bank="Bank#1")
|
||||||
]
|
]
|
||||||
|
|
||||||
for t in transactions:
|
for t in transactions:
|
||||||
@ -94,7 +94,7 @@ class TestTransform:
|
|||||||
|
|
||||||
def test_categorize(self):
|
def test_categorize(self):
|
||||||
transactions = [
|
transactions = [
|
||||||
BankTransaction(date(2023, 1, 1), "desc#1", Decimal("-10"), "Bank#1")
|
BankTransaction(date(2023, 1, 1), "desc#1", Decimal("-10"), bank="Bank#1")
|
||||||
]
|
]
|
||||||
|
|
||||||
for t in transactions:
|
for t in transactions:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user