From f7df033d5834930c093a6c47e43b0e982ddad60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Murta?= Date: Mon, 6 Feb 2023 22:10:53 +0000 Subject: [PATCH] Add start date rule Rename date to end. --- .../952de57a3c43_start_end_date_rule.py | 32 +++++++++++ pfbudget/__main__.py | 13 +++-- pfbudget/cli/argparser.py | 3 +- pfbudget/db/model.py | 53 +++++++++++-------- 4 files changed, 70 insertions(+), 31 deletions(-) create mode 100644 alembic/versions/952de57a3c43_start_end_date_rule.py diff --git a/alembic/versions/952de57a3c43_start_end_date_rule.py b/alembic/versions/952de57a3c43_start_end_date_rule.py new file mode 100644 index 0000000..e9590b8 --- /dev/null +++ b/alembic/versions/952de57a3c43_start_end_date_rule.py @@ -0,0 +1,32 @@ +"""Start/End date rule + +Revision ID: 952de57a3c43 +Revises: 18572111d9ff +Create Date: 2023-02-06 21:57:57.545327+00:00 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "952de57a3c43" +down_revision = "18572111d9ff" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "rules", sa.Column("start", sa.Date(), nullable=True), schema="transactions" + ) + op.alter_column("rules", column_name="date", new_column_name="end", schema="transactions") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("rules", column_name="end", new_column_name="date", schema="transactions") + op.drop_column("rules", "start", schema="transactions") + # ### end Alembic commands ### diff --git a/pfbudget/__main__.py b/pfbudget/__main__.py index 228a83b..0132d84 100644 --- a/pfbudget/__main__.py +++ b/pfbudget/__main__.py @@ -1,6 +1,3 @@ -from decimal import Decimal -from typing import Sequence - from pfbudget.cli.argparser import argparser from pfbudget.cli.interactive import Interactive from pfbudget.common.types import Operation @@ -146,12 +143,13 @@ if __name__ == "__main__": ] case Operation.RuleAdd: - keys = {"category", "date", "description", "bank", "min", "max"} + keys = {"category", "start", "end", "description", "regex", "bank", "min", "max"} assert args.keys() >= keys, f"missing {args.keys() - keys}" params = [ type.CategoryRule( - args["date"][0] if args["date"] else None, + args["start"][0] if args["start"] else None, + args["end"][0] if args["end"] 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, @@ -197,12 +195,13 @@ if __name__ == "__main__": params = [type.Tag(tag) for tag in args["tag"]] case Operation.TagRuleAdd: - keys = {"tag", "date", "description", "bank", "min", "max"} + keys = {"tag", "start", "end", "description", "regex", "bank", "min", "max"} assert args.keys() >= keys, f"missing {args.keys() - keys}" params = [ type.TagRule( - args["date"][0] if args["date"] else None, + args["start"][0] if args["start"] else None, + args["end"][0] if args["end"] 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, diff --git a/pfbudget/cli/argparser.py b/pfbudget/cli/argparser.py index 9029920..1e47aa6 100644 --- a/pfbudget/cli/argparser.py +++ b/pfbudget/cli/argparser.py @@ -374,7 +374,8 @@ def tag_rule(parser: argparse.ArgumentParser): def rules(parser: argparse.ArgumentParser): - parser.add_argument("--date", nargs=1, type=dt.date.fromisoformat) + parser.add_argument("--start", nargs=1, type=dt.date.fromisoformat) + parser.add_argument("--end", 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) diff --git a/pfbudget/db/model.py b/pfbudget/db/model.py index dcd1969..eb2d712 100644 --- a/pfbudget/db/model.py +++ b/pfbudget/db/model.py @@ -335,7 +335,8 @@ class Rule(Base, Export): __tablename__ = "rules" id: Mapped[idpk] = mapped_column(init=False) - date: Mapped[Optional[dt.date]] + start: Mapped[Optional[dt.date]] + end: Mapped[Optional[dt.date]] description: Mapped[Optional[str]] regex: Mapped[Optional[str]] bank: Mapped[Optional[str]] @@ -349,32 +350,34 @@ class Rule(Base, Export): "polymorphic_on": "type", } - def matches(self, transaction: BankTransaction) -> bool: - if ( - (self.date and self.date < transaction.date) - or ( - self.description - and transaction.description - and self.description not in transaction.description - ) - or ( - self.regex - and transaction.description - and not re.compile(self.regex, re.IGNORECASE).search( - transaction.description - ) - ) - or (self.bank and self.bank != transaction.bank) - or (self.min and self.min > transaction.amount) - or (self.max and self.max < transaction.amount) - ): - return False - return True + def matches(self, t: BankTransaction) -> bool: + valid = None + if self.regex: + valid = re.compile(self.regex, re.IGNORECASE) + + ops = ( + Rule.exists(self.start, lambda r: r < t.date), + Rule.exists(self.end, lambda r: r > t.date), + Rule.exists(self.description, lambda r: r == t.description), + Rule.exists( + valid, + lambda r: r.search(t.description) if t.description else False, + ), + Rule.exists(self.bank, lambda r: r == t.bank), + Rule.exists(self.min, lambda r: r < t.amount), + Rule.exists(self.max, lambda r: r > t.amount), + ) + + if all(ops): + return True + + return False @property def format(self) -> dict[str, Any]: return dict( - date=self.date, + start=self.start, + end=self.end, description=self.description, regex=self.regex, bank=self.bank, @@ -383,6 +386,10 @@ class Rule(Base, Export): type=self.type, ) + @staticmethod + def exists(r, op) -> bool: + return op(r) if r is not None else True + class CategoryRule(Rule): __table_args__ = {"schema": "category"}