diff --git a/alembic/versions/8cc9870b0d74_links.py b/alembic/versions/8cc9870b0d74_links.py new file mode 100644 index 0000000..7043f81 --- /dev/null +++ b/alembic/versions/8cc9870b0d74_links.py @@ -0,0 +1,46 @@ +"""Links + +Revision ID: 8cc9870b0d74 +Revises: a910e1b2214d +Create Date: 2022-12-19 22:10:25.136479+00:00 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "8cc9870b0d74" +down_revision = "a910e1b2214d" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "links", + sa.Column("original", sa.BigInteger(), nullable=False), + sa.Column("link", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["link"], + ["transactions.originals.id"], + name=op.f("fk_links_link_originals"), + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["original"], + ["transactions.originals.id"], + name=op.f("fk_links_original_originals"), + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("original", "link", name=op.f("pk_links")), + schema="transactions", + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("links", schema="transactions") + # ### end Alembic commands ### diff --git a/pfbudget/__main__.py b/pfbudget/__main__.py index 4f3f2f9..c62c96f 100644 --- a/pfbudget/__main__.py +++ b/pfbudget/__main__.py @@ -149,4 +149,10 @@ if __name__ == "__main__": assert "group" in args, "argparser ill defined" params = [pfbudget.types.CategoryGroup(group) for group in args["group"]] + case pfbudget.Operation.Forge | pfbudget.Operation.Dismantle: + assert args.keys() >= {"original", "links"}, "argparser ill defined" + params = [ + pfbudget.types.Link(args["original"][0], link) for link in args["links"] + ] + pfbudget.Manager(db, verbosity, args).action(op, params) diff --git a/pfbudget/cli/runnable.py b/pfbudget/cli/runnable.py index 8d62abc..aa780f2 100644 --- a/pfbudget/cli/runnable.py +++ b/pfbudget/cli/runnable.py @@ -5,9 +5,7 @@ import decimal import re from pfbudget.common.types import Operation -from pfbudget.core.categories import categorize_data from pfbudget.db.model import Period -from pfbudget.input.json import JsonParser from pfbudget.input.nordigen import NordigenInput from pfbudget.db.sqlite import DatabaseClient import pfbudget.reporting.graph @@ -253,6 +251,9 @@ def argparser() -> argparse.ArgumentParser: # Tag tags(subparsers.add_parser("tag", parents=[universal]), universal) + # Link + link(subparsers.add_parser("link")) + return parser @@ -420,6 +421,20 @@ def rules(parser: argparse.ArgumentParser): parser.add_argument("--max", nargs=1, type=decimal.Decimal) +def link(parser: argparse.ArgumentParser): + commands = parser.add_subparsers(required=True) + + forge = commands.add_parser("forge") + forge.set_defaults(op=Operation.Forge) + forge.add_argument("original", nargs=1, type=int) + forge.add_argument("links", nargs="+", type=int) + + dismantle = commands.add_parser("dismantle") + dismantle.set_defaults(op=Operation.Dismantle) + dismantle.add_argument("original", nargs=1, type=int) + dismantle.add_argument("links", nargs="+", type=int) + + def run(): args = vars(argparser().parse_args()) assert "op" in args, "No operation selected" diff --git a/pfbudget/common/types.py b/pfbudget/common/types.py index 8a6c0e4..0213c03 100644 --- a/pfbudget/common/types.py +++ b/pfbudget/common/types.py @@ -27,6 +27,8 @@ class Operation(Enum): TagRuleAdd = auto() TagRuleRemove = auto() TagRuleModify = auto() + Forge = auto() + Dismantle = auto() class TransactionError(Exception): diff --git a/pfbudget/core/manager.py b/pfbudget/core/manager.py index f143951..b2c516e 100644 --- a/pfbudget/core/manager.py +++ b/pfbudget/core/manager.py @@ -105,6 +105,16 @@ class Manager: with self.db.session() as session: session.remove_by_name(CategoryGroup, params) + case Operation.Forge: + with self.db.session() as session: + session.add(params) + + case Operation.Dismantle: + with self.db.session() as session: + original = params[0].original + links = [link.link for link in params] + session.remove_links(original, links) + # def init(self): # client = DatabaseClient(self.__db) # client.init() diff --git a/pfbudget/db/client.py b/pfbudget/db/client.py index c29cf56..5af6181 100644 --- a/pfbudget/db/client.py +++ b/pfbudget/db/client.py @@ -1,4 +1,3 @@ -from copy import deepcopy from dataclasses import asdict from sqlalchemy import create_engine, delete, select, update from sqlalchemy.dialects.postgresql import insert @@ -10,6 +9,7 @@ from pfbudget.db.model import ( CategoryGroup, CategoryRule, CategorySchedule, + Link, Tag, TagRule, Transaction, @@ -83,12 +83,7 @@ class DbClient: def commit(self): self.__session.commit() - def add( - self, - rows: list[ - Category | CategoryGroup | CategoryRule | Tag | TagRule | Transaction - ], - ): + def add(self, rows: list): self.__session.add_all(rows) def remove_by_name(self, type: Category | Tag | Transaction, rows: list): @@ -122,6 +117,10 @@ class DbClient: def updaterules(self, rules: list[dict]): self.__session.execute(update(CategoryRule), rules) + def remove_links(self, original, links: list): + stmt = delete(Link).where(Link.original == original, Link.link.in_(link for link in links)) + self.__session.execute(stmt) + def uncategorized(self) -> list[Transaction]: stmt = select(Transaction).where(~Transaction.category.has()) return self.__session.scalars(stmt).all() diff --git a/pfbudget/db/model.py b/pfbudget/db/model.py index 7ef354d..5211cf1 100644 --- a/pfbudget/db/model.py +++ b/pfbudget/db/model.py @@ -63,9 +63,6 @@ class Bank(Base): back_populates="bank", lazy="joined" ) - def __repr__(self) -> str: - return f"Bank(name={self.name}, BIC={self.BIC}, type={self.type}, nordigen={self.nordigen})" - bankfk = Annotated[str, mapped_column(Text, ForeignKey(Bank.name))] @@ -83,13 +80,9 @@ class Transaction(Base): amount: Mapped[money] category: Mapped[Optional[TransactionCategory]] = relationship() - ) note: Mapped[Optional[Note]] = relationship(back_populates="original") tags: Mapped[Optional[set[TransactionTag]]] = relationship() - def __repr__(self) -> str: - return f"Transaction(date={self.date}, description={self.description}, bank={self.bank}, amount={self.amount}, category={self.category})" - idfk = Annotated[ int, mapped_column(BigInteger, ForeignKey(Transaction.id, ondelete="CASCADE")) @@ -158,9 +151,6 @@ class Nordigen(Base): bank: Mapped[Bank] = relationship(back_populates="nordigen") - def __repr__(self) -> str: - return f"(bank_id={self.bank_id}, requisition_id={self.requisition_id}, invert={self.invert})" - class Tag(Base): __tablename__ = "tags_available" @@ -229,8 +219,12 @@ class CategorySchedule(Base): category: Mapped[Category] = relationship(back_populates="schedule") - def __repr__(self) -> str: - return f"{self.name} schedule=Schedule(period={self.period}, multiplier={self.period_multiplier}, amount={self.amount})" + +class Link(Base): + __tablename__ = "links" + + original: Mapped[idfk] = mapped_column(primary_key=True) + link: Mapped[idfk] = mapped_column(primary_key=True) class Rule: