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:
parent
6c33a94a5f
commit
058d0cc05d
68
alembic/versions/9028b0f3b985_tag_rules.py
Normal file
68
alembic/versions/9028b0f3b985_tag_rules.py
Normal 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 ###
|
||||
@ -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"]]
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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]]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user