Compare commits

...

3 Commits

Author SHA1 Message Date
8fe0ecc597
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.
2022-12-04 16:09:54 +00:00
78e545589d
Adds category groups 2022-12-04 15:14:24 +00:00
b8142f4f99
Add available categories table 2022-12-04 11:19:21 +00:00
6 changed files with 183 additions and 5 deletions

View File

@ -0,0 +1,49 @@
"""Available categories and rules
Revision ID: 2d0891f1be11
Revises: 287fe9e6682a
Create Date: 2022-12-04 11:15:22.758487+00:00
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "2d0891f1be11"
down_revision = "287fe9e6682a"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"categories_available",
sa.Column("name", sa.String(), nullable=False),
sa.PrimaryKeyConstraint("name", name=op.f("pk_categories_available")),
schema="transactions",
)
op.create_table(
"categories_rules",
sa.Column("name", sa.String(), nullable=False),
sa.Column("rule", sa.String(), nullable=False),
sa.ForeignKeyConstraint(
["name"],
["transactions.categories_available.name"],
name=op.f("fk_categories_rules_name_categories_available"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("name", "rule", name=op.f("pk_categories_rules")),
schema="transactions",
)
op.alter_column("categorized", "category", new_column_name="name", schema="transactions")
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column("categorized", "name", new_column_name="category", schema="transactions")
op.drop_table("categories_rules", schema="transactions")
op.drop_table("categories_available", schema="transactions")
# ### end Alembic commands ###

View File

@ -0,0 +1,69 @@
"""Category groups and relationships
Revision ID: 83f4c9837f6e
Revises: 2d0891f1be11
Create Date: 2022-12-04 15:10:51.924875+00:00
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "83f4c9837f6e"
down_revision = "2d0891f1be11"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"categories_groups",
sa.Column("name", sa.String(), nullable=False),
sa.PrimaryKeyConstraint("name", name=op.f("pk_categories_groups")),
schema="transactions",
)
op.add_column(
"categories_available",
sa.Column("group", sa.String(), nullable=True),
schema="transactions",
)
op.create_foreign_key(
op.f("fk_categories_available_group_categories_groups"),
"categories_available",
"categories_groups",
["group"],
["name"],
source_schema="transactions",
referent_schema="transactions",
)
op.create_foreign_key(
op.f("fk_categorized_name_categories_available"),
"categorized",
"categories_available",
["name"],
["name"],
source_schema="transactions",
referent_schema="transactions",
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(
op.f("fk_categorized_name_categories_available"),
"categorized",
schema="transactions",
type_="foreignkey",
)
op.drop_constraint(
op.f("fk_categories_available_group_categories_groups"),
"categories_available",
schema="transactions",
type_="foreignkey",
)
op.drop_column("categories_available", "group", schema="transactions")
op.drop_table("categories_groups", schema="transactions")
# ### end Alembic commands ###

View File

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

View File

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

View File

@ -1,7 +1,7 @@
from sqlalchemy import create_engine, select from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session, joinedload, selectinload from sqlalchemy.orm import Session, joinedload, selectinload
from pfbudget.db.model import Bank, Transaction from pfbudget.db.model import Bank, Category, Transaction
# import logging # import logging
@ -55,3 +55,30 @@ class DbClient:
@property @property
def engine(self): def engine(self):
return self._engine 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

@ -75,7 +75,7 @@ class Transaction(Base):
bank: Mapped[bankfk] bank: Mapped[bankfk]
amount: Mapped[money] amount: Mapped[money]
category: Mapped[Optional[Category]] = relationship( category: Mapped[Optional[TransactionCategory]] = relationship(
back_populates="original", lazy="joined" back_populates="original", lazy="joined"
) )
note: Mapped[Optional[Note]] = relationship(back_populates="original") note: Mapped[Optional[Note]] = relationship(back_populates="original")
@ -92,16 +92,33 @@ idfk = Annotated[
] ]
class CategoryGroup(Base):
__tablename__ = "categories_groups"
name: Mapped[str] = mapped_column(primary_key=True)
class Category(Base): class Category(Base):
__tablename__ = "categories_available"
name: Mapped[str] = mapped_column(primary_key=True)
group: Mapped[Optional[str]] = mapped_column(ForeignKey(CategoryGroup.name))
rules: Mapped[Optional[set[CategoryRule]]] = relationship(
cascade="all, delete-orphan", passive_deletes=True
)
class TransactionCategory(Base):
__tablename__ = "categorized" __tablename__ = "categorized"
id: Mapped[idfk] = mapped_column(primary_key=True) id: Mapped[idfk] = mapped_column(primary_key=True)
category: Mapped[str] name: Mapped[str] = mapped_column(ForeignKey(Category.name))
original: Mapped[Transaction] = relationship(back_populates="category") original: Mapped[Transaction] = relationship(back_populates="category")
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Category({self.category})" return f"Category({self.name})"
class Note(Base): class Note(Base):
@ -134,3 +151,12 @@ class Tag(Base):
tag: Mapped[str] = mapped_column(primary_key=True) tag: Mapped[str] = mapped_column(primary_key=True)
original: Mapped[Transaction] = relationship(back_populates="tags") original: Mapped[Transaction] = relationship(back_populates="tags")
class CategoryRule(Base):
__tablename__ = "categories_rules"
name: Mapped[str] = mapped_column(
ForeignKey(Category.name, ondelete="CASCADE"), primary_key=True
)
rule: Mapped[str] = mapped_column(primary_key=True)