Adds Tags rules

Creates an available tags table to cross reference to.
Clarifies some logic in the DbClient, since all adds/removes are the
same.
This commit is contained in:
Luís Murta 2022-12-18 23:38:45 +00:00
parent 6c33a94a5f
commit 058d0cc05d
Signed by: satprog
GPG Key ID: 169EF1BBD7049F94
7 changed files with 240 additions and 52 deletions

View File

@ -0,0 +1,68 @@
"""Tag rules
Revision ID: 9028b0f3b985
Revises: 753c0bfb2062
Create Date: 2022-12-18 22:53:13.334046+00:00
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "9028b0f3b985"
down_revision = "753c0bfb2062"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"tags_available",
sa.Column("name", sa.String(), nullable=False),
sa.PrimaryKeyConstraint("name", name=op.f("pk_tags_available")),
schema="transactions",
)
op.create_table(
"tag_rules",
sa.Column("id", sa.BigInteger(), nullable=False),
sa.Column("tag", sa.String(), nullable=False),
sa.Column("date", sa.Date(), nullable=True),
sa.Column("description", sa.String(), nullable=True),
sa.Column("regex", sa.String(), nullable=True),
sa.Column("bank", sa.String(), nullable=True),
sa.Column("min", sa.Numeric(precision=16, scale=2), nullable=True),
sa.Column("max", sa.Numeric(precision=16, scale=2), nullable=True),
sa.ForeignKeyConstraint(
["tag"],
["transactions.tags_available.name"],
name=op.f("fk_tag_rules_tag_tags_available"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_tag_rules")),
schema="transactions",
)
op.create_foreign_key(
op.f("fk_tags_tag_tags_available"),
"tags",
"tags_available",
["tag"],
["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_tags_tag_tags_available"),
"tags",
schema="transactions",
type_="foreignkey",
)
op.drop_table("tag_rules", schema="transactions")
op.drop_table("tags_available", schema="transactions")
# ### end Alembic commands ###

View File

@ -65,7 +65,7 @@ if __name__ == "__main__":
for cat in args["category"]
]
case pfbudget.Operation.RuleRemove:
case pfbudget.Operation.RuleRemove | pfbudget.Operation.TagRuleRemove:
assert args.keys() >= {"id"}, "argparser ill defined"
params = args["id"]
@ -90,6 +90,54 @@ if __name__ == "__main__":
params.append(param)
case pfbudget.Operation.TagAdd:
assert args.keys() >= {"tag"}, "argparser ill defined"
params = [pfbudget.types.Tag(tag) for tag in args["tag"]]
case pfbudget.Operation.TagRuleAdd:
assert args.keys() >= {
"tag",
"date",
"description",
"bank",
"min",
"max",
}, "argparser ill defined"
params = [
pfbudget.types.TagRule(
tag,
args["date"][0] if args["date"] 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,
)
for tag in args["tag"]
]
case pfbudget.Operation.TagRuleModify:
assert args.keys() >= {
"id",
"tag",
"date",
"description",
"bank",
"min",
"max",
"remove",
}, "argparser ill defined"
nargs_1 = ["tag", "date", "description", "regex", "bank", "min", "max"]
params = []
for id in args["id"]:
param = {"id": id}
param |= {k: v[0] for k, v in args.items() if k in nargs_1 and args[k]}
param |= {k: None for k in args["remove"] if k in nargs_1}
params.append(param)
case pfbudget.Operation.GroupAdd:
assert "group" in args, "argparser ill defined"
params = [pfbudget.types.CategoryGroup(group) for group in args["group"]]

View File

@ -255,6 +255,9 @@ def argparser() -> argparse.ArgumentParser:
category_parser = subparsers.add_parser("category", parents=[universal])
category(category_parser, universal)
# Tag
tags(subparsers.add_parser("tag", parents=[universal]), universal)
return parser
@ -366,12 +369,7 @@ def category_rule(parser: argparse.ArgumentParser, universal: argparse.ArgumentP
add = commands.add_parser("add", parents=[universal])
add.set_defaults(op=Operation.RuleAdd)
add.add_argument("category", nargs="+", type=str)
add.add_argument("--date", nargs=1, type=dt.date.fromisoformat)
add.add_argument("--description", nargs=1, type=str)
add.add_argument("--regex", nargs=1, type=str)
add.add_argument("--bank", nargs=1, type=str)
add.add_argument("--min", nargs=1, type=decimal.Decimal)
add.add_argument("--max", nargs=1, type=decimal.Decimal)
rules(add)
remove = commands.add_parser("remove", parents=[universal])
remove.set_defaults(op=Operation.RuleRemove)
@ -381,13 +379,51 @@ def category_rule(parser: argparse.ArgumentParser, universal: argparse.ArgumentP
modify.set_defaults(op=Operation.RuleModify)
modify.add_argument("id", nargs="+", type=int)
modify.add_argument("--category", nargs=1, type=str)
modify.add_argument("--date", nargs=1, type=dt.date.fromisoformat)
modify.add_argument("--description", nargs=1, type=str)
modify.add_argument("--regex", nargs=1, type=str)
modify.add_argument("--bank", nargs=1, type=str)
modify.add_argument("--min", nargs=1, type=decimal.Decimal)
modify.add_argument("--max", nargs=1, type=decimal.Decimal)
modify.add_argument("--remove", nargs="*", default=[], type=str)
rules(modify)
def tags(parser: argparse.ArgumentParser, universal: argparse.ArgumentParser):
commands = parser.add_subparsers(required=True)
add = commands.add_parser("add", parents=[universal])
add.set_defaults(op=Operation.TagAdd)
add.add_argument("tag", nargs="+", type=str)
remove = commands.add_parser("remove", parents=[universal])
remove.set_defaults(op=Operation.TagRemove)
remove.add_argument("tag", nargs="+", type=str)
rule = commands.add_parser("rule", parents=[universal])
tag_rule(rule, universal)
def tag_rule(parser: argparse.ArgumentParser, universal: argparse.ArgumentParser):
commands = parser.add_subparsers(required=True)
add = commands.add_parser("add", parents=[universal])
add.set_defaults(op=Operation.TagRuleAdd)
add.add_argument("tag", nargs="+", type=str)
rules(add)
remove = commands.add_parser("remove", parents=[universal])
remove.set_defaults(op=Operation.TagRuleRemove)
remove.add_argument("id", nargs="+", type=int)
modify = commands.add_parser("modify", parents=[universal])
modify.set_defaults(op=Operation.TagRuleModify)
modify.add_argument("id", nargs="+", type=int)
modify.add_argument("--tag", nargs=1, type=str)
rules(modify)
def rules(parser: argparse.ArgumentParser):
parser.add_argument("--date", nargs=1, type=dt.date.fromisoformat)
parser.add_argument("--description", nargs=1, type=str)
parser.add_argument("--regex", nargs=1, type=str)
parser.add_argument("--bank", nargs=1, type=str)
parser.add_argument("--min", nargs=1, type=decimal.Decimal)
parser.add_argument("--max", nargs=1, type=decimal.Decimal)
def run():

View File

@ -22,6 +22,11 @@ class Operation(Enum):
RuleModify = auto()
GroupAdd = auto()
GroupRemove = auto()
TagAdd = auto()
TagRemove = auto()
TagRuleAdd = auto()
TagRuleRemove = auto()
TagRuleModify = auto()
class TransactionError(Exception):

View File

@ -2,7 +2,14 @@ from pfbudget.input.input import Input
from pfbudget.input.nordigen import NordigenClient
from pfbudget.input.parsers import parse_data
from pfbudget.db.client import DbClient
from pfbudget.db.model import Category, CategoryGroup, CategoryRule, CategorySchedule
from pfbudget.db.model import (
Category,
CategoryGroup,
CategoryRule,
CategorySchedule,
Tag,
TagRule,
)
from pfbudget.common.types import Operation
from pfbudget.core.categorizer import Categorizer
from pfbudget.utils import convert
@ -49,9 +56,9 @@ class Manager:
self.args["name"], self.args["country"]
)
case Operation.CategoryAdd:
case Operation.CategoryAdd | Operation.TagAdd:
with self.db.session() as session:
session.addcategories(params)
session.add(params)
case Operation.CategoryUpdate:
with self.db.session() as session:
@ -59,34 +66,43 @@ class Manager:
case Operation.CategoryRemove:
with self.db.session() as session:
session.removecategories(params)
session.remove_by_name(Category, params)
case Operation.CategorySchedule:
with self.db.session() as session:
session.updateschedules(params)
case Operation.RuleAdd:
case Operation.RuleAdd | Operation.TagRuleAdd:
with self.db.session() as session:
session.addrules(params)
session.add(params)
case Operation.RuleRemove:
assert all(isinstance(param, int) for param in params)
with self.db.session() as session:
session.removerules(params)
session.remove_by_id(CategoryRule, params)
case Operation.RuleModify:
case Operation.TagRemove:
with self.db.session() as session:
session.remove_by_name(Tag, params)
case Operation.TagRuleRemove:
assert all(isinstance(param, int) for param in params)
with self.db.session() as session:
session.remove_by_id(TagRule, params)
case Operation.RuleModify | Operation.TagRuleModify:
assert all(isinstance(param, dict) for param in params)
with self.db.session() as session:
session.updaterules(params)
case Operation.GroupAdd:
with self.db.session() as session:
session.addgroups(CategoryGroup(params))
session.add(CategoryGroup(params))
case Operation.GroupRemove:
assert all(isinstance(param, CategoryGroup) for param in params)
with self.db.session() as session:
session.removegroups(params)
session.remove_by_name(CategoryGroup, params)
# def init(self):
# client = DatabaseClient(self.__db)

View File

@ -10,6 +10,8 @@ from pfbudget.db.model import (
CategoryGroup,
CategoryRule,
CategorySchedule,
Tag,
TagRule,
Transaction,
)
@ -81,16 +83,16 @@ class DbClient:
def commit(self):
self.__session.commit()
def add(self, transactions: list[Transaction]):
self.__session.add_all(transactions)
def add(
self,
rows: list[
Category | CategoryGroup | CategoryRule | Tag | TagRule | Transaction
],
):
self.__session.add_all(rows)
def addcategories(self, category: list[Category]):
self.__session.add_all(category)
def removecategories(self, categories: list[Category]):
stmt = delete(Category).where(
Category.name.in_([cat.name for cat in categories])
)
def remove_by_name(self, type: Category | Tag | Transaction, rows: list):
stmt = delete(type).where(type.name.in_([row.name for row in rows]))
self.__session.execute(stmt)
def updategroup(self, categories: list[Category], group: CategoryGroup):
@ -113,25 +115,13 @@ class DbClient:
)
self.__session.execute(stmt)
def addrules(self, rules: list[CategoryRule]):
self.__session.add_all(rules)
def removerules(self, ids: list[int]):
stmt = delete(CategoryRule).where(CategoryRule.id.in_(ids))
def remove_by_id(self, type: CategoryRule | TagRule, ids: list[int]):
stmt = delete(type).where(type.id.in_(ids))
self.__session.execute(stmt)
def updaterules(self, rules: list[dict]):
self.__session.execute(update(CategoryRule), rules)
def addgroups(self, groups: list[CategoryGroup]):
self.__session.add_all(groups)
def removegroups(self, groups: list[CategoryGroup]):
stmt = delete(CategoryGroup).where(
CategoryGroup.name.in_([grp.name for grp in groups])
)
self.__session.execute(stmt)
def uncategorized(self) -> list[Transaction]:
stmt = select(Transaction).where(~Transaction.category.has())
return self.__session.scalars(stmt).all()

View File

@ -68,14 +68,16 @@ class Bank(Base):
bankfk = Annotated[str, mapped_column(Text, ForeignKey(Bank.name))]
idpk = Annotated[int, mapped_column(BigInteger, primary_key=True)]
idpk = Annotated[
int, mapped_column(BigInteger, primary_key=True, autoincrement=True, init=False)
]
money = Annotated[Decimal, mapped_column(Numeric(16, 2))]
class Transaction(Base):
__tablename__ = "originals"
id: Mapped[idpk] = mapped_column(autoincrement=True, init=False)
id: Mapped[idpk]
date: Mapped[dt.date]
description: Mapped[Optional[str]]
bank: Mapped[bankfk]
@ -83,7 +85,7 @@ class Transaction(Base):
category: Mapped[Optional[TransactionCategory]] = relationship()
note: Mapped[Optional[Note]] = relationship(back_populates="original", default=None)
tags: Mapped[Optional[set[Tag]]] = relationship(
tags: Mapped[Optional[set[TransactionTag]]] = relationship(
back_populates="original",
cascade="all, delete-orphan",
passive_deletes=True,
@ -166,10 +168,20 @@ class Nordigen(Base):
class Tag(Base):
__tablename__ = "tags_available"
name: Mapped[str] = mapped_column(primary_key=True)
rules: Mapped[Optional[set[TagRule]]] = relationship(
cascade="all, delete-orphan", passive_deletes=True, default_factory=set
)
class TransactionTag(Base):
__tablename__ = "tags"
id: Mapped[idfk] = mapped_column(primary_key=True)
tag: Mapped[str] = mapped_column(primary_key=True)
tag: Mapped[str] = mapped_column(ForeignKey(Tag.name), primary_key=True)
original: Mapped[Transaction] = relationship(back_populates="tags")
@ -177,7 +189,7 @@ class Tag(Base):
class CategoryRule(Base):
__tablename__ = "categories_rules"
id: Mapped[idpk] = mapped_column(autoincrement=True, init=False)
id: Mapped[idpk]
name: Mapped[catfk]
date: Mapped[Optional[dt.date]]
description: Mapped[Optional[str]]
@ -239,3 +251,16 @@ class CategorySchedule(Base):
def __repr__(self) -> str:
return f"{self.name} schedule=Schedule(period={self.period}, multiplier={self.period_multiplier}, amount={self.amount})"
class TagRule(Base):
__tablename__ = "tag_rules"
id: Mapped[idpk]
tag: Mapped[str] = mapped_column(ForeignKey(Tag.name, ondelete="CASCADE"))
date: Mapped[Optional[dt.date]]
description: Mapped[Optional[str]]
regex: Mapped[Optional[str]]
bank: Mapped[Optional[str]]
min: Mapped[Optional[money]]
max: Mapped[Optional[money]]