Introduces categorizer that works on ORM classes

Categorizer will work directly on ORM classes, which will cleanup the
code, since changes will automatically be persisted when change the
objects.

Adds wrapper session class inside the DbClient for the manager to use.
The manager will have to have some DB session knowledge, which adds some
unfortunate coupling.

Removes some unnecessary relations between tables that were added by
mistake.

category CLI option now uses the manager.
This commit is contained in:
Luís Murta 2022-12-04 16:09:54 +00:00
parent 78e545589d
commit be67612f67
Signed by: satprog
GPG Key ID: 169EF1BBD7049F94
5 changed files with 80 additions and 7 deletions

View File

@ -112,7 +112,7 @@ def argparser(manager: Manager) -> argparse.ArgumentParser:
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
p_categorize.set_defaults(
func=lambda args: categorize_data(DatabaseClient(args.database))
func=lambda args: manager.categorize(vars(args))
)
"""

View File

@ -0,0 +1,43 @@
from pfbudget.db.model import Transaction, TransactionCategory
from datetime import timedelta
class Categorizer:
options = {}
def __init__(self, args: dict):
self.options["null_days"] = 4
def categorize(self, transactions: list[Transaction]):
"""Overarching categorization tool
Receives a list of transactions (by ref) and updates their category
Args:
transactions (list[Transaction]): uncategorized transactions
"""
self._nullify(transactions)
def _nullify(self, transactions: list[Transaction]):
matching = []
for transaction in transactions:
for cancel in (
cancel
for cancel in transactions
if (
transaction.date - timedelta(days=self.options["null_days"])
<= cancel.date
<= transaction.date + timedelta(days=self.options["null_days"])
and transaction not in matching
and cancel not in matching
and cancel != transaction
and cancel.bank != transaction.bank
and cancel.amount == -transaction.amount
)
):
transaction.category = TransactionCategory(name="null")
cancel.category = TransactionCategory(name="null")
matching.extend([transaction, cancel])
break

View File

@ -1,6 +1,7 @@
from pfbudget.input.input import Input
from pfbudget.input.parsers import parse_data
from pfbudget.db.client import DbClient
from pfbudget.core.categorizer import Categorizer
from pfbudget.utils import convert
@ -38,6 +39,12 @@ class Manager:
session.add(transactions)
session.commit()
def categorize(self, args: dict):
with self.db.session() as session:
uncategorized = session.uncategorized()
Categorizer().categorize(uncategorized)
session.commit()
# def get_bank_by(self, key: str, value: str) -> Bank:
# client = DatabaseClient(self.__db)
# bank = client.get_bank(key, value)

View File

@ -1,7 +1,7 @@
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session, joinedload, selectinload
from pfbudget.db.model import Bank, Transaction
from pfbudget.db.model import Bank, Category, Transaction
# import logging
@ -55,3 +55,30 @@ class DbClient:
@property
def engine(self):
return self._engine
class ClientSession:
def __init__(self, engine):
self.__engine = engine
def __enter__(self):
self.__session = Session(self.__engine)
return self
def __exit__(self, exc_type, exc_value, exc_tb):
self.__session.close()
def commit(self):
self.__session.commit()
def add(self, transactions: list[Transaction]):
self.__session.add_all(transactions)
def addcategory(self, category: Category):
self.__session.add(category)
def uncategorized(self) -> list[Transaction]:
stmt = select(Transaction).where(~Transaction.category.has())
return self.__session.scalars(stmt).all()
def session(self):
return self.ClientSession(self.engine)

View File

@ -105,9 +105,8 @@ class Category(Base):
group: Mapped[Optional[str]] = mapped_column(ForeignKey(CategoryGroup.name))
rules: Mapped[Optional[set[CategoryRule]]] = relationship(
back_populates="category", cascade="all, delete-orphan", passive_deletes=True
cascade="all, delete-orphan", passive_deletes=True
)
categorygroup: Mapped[Optional[CategoryGroup]] = relationship()
class TransactionCategory(Base):
@ -117,7 +116,6 @@ class TransactionCategory(Base):
name: Mapped[str] = mapped_column(ForeignKey(Category.name))
original: Mapped[Transaction] = relationship(back_populates="category")
category: Mapped[Category] = relationship()
def __repr__(self) -> str:
return f"Category({self.name})"
@ -162,5 +160,3 @@ class CategoryRule(Base):
ForeignKey(Category.name, ondelete="CASCADE"), primary_key=True
)
rule: Mapped[str] = mapped_column(primary_key=True)
category: Mapped[Category] = relationship(back_populates="rules")