From c37e7eb37cb7fbd8edd69c3a01ecfd6444da995f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Murta?= Date: Tue, 10 Jan 2023 21:32:08 +0000 Subject: [PATCH] Readds manual categorization Also fixes a categorization bug in the Manager, in the DB client method. --- pfbudget/cli/runnable.py | 13 +++----- pfbudget/common/types.py | 1 + pfbudget/core/categorizer.py | 64 +++++++++++++++++++++++++++++++----- pfbudget/core/manager.py | 11 +++++-- pfbudget/db/client.py | 4 +-- 5 files changed, 71 insertions(+), 22 deletions(-) diff --git a/pfbudget/cli/runnable.py b/pfbudget/cli/runnable.py index 6b5fb42..f41187d 100644 --- a/pfbudget/cli/runnable.py +++ b/pfbudget/cli/runnable.py @@ -87,15 +87,10 @@ def argparser() -> argparse.ArgumentParser: parse.add_argument("--bank", nargs=1, type=str) parse.add_argument("--creditcard", nargs=1, type=str) - """ - Categorizing - """ - categorize = subparsers.add_parser( - "categorize", - description="Categorizes the transactions in the selected database", - parents=[universal], - ) - categorize.set_defaults(op=Operation.Categorize) + # Automatic/manual categorization + categorize = subparsers.add_parser("categorize").add_subparsers(required=True) + categorize.add_parser("auto").set_defaults(op=Operation.Categorize) + categorize.add_parser("manual").set_defaults(op=Operation.ManualCategorization) """ Graph diff --git a/pfbudget/common/types.py b/pfbudget/common/types.py index 71a763f..2f98d41 100644 --- a/pfbudget/common/types.py +++ b/pfbudget/common/types.py @@ -9,6 +9,7 @@ class Operation(Enum): Parse = auto() Download = auto() Categorize = auto() + ManualCategorization = auto() Token = auto() RequisitionId = auto() CategoryAdd = auto() diff --git a/pfbudget/core/categorizer.py b/pfbudget/core/categorizer.py index fb7405a..d2918e3 100644 --- a/pfbudget/core/categorizer.py +++ b/pfbudget/core/categorizer.py @@ -10,6 +10,8 @@ from pfbudget.db.model import ( from datetime import timedelta +Transactions = list[Transaction] + class Categorizer: options = {} @@ -17,25 +19,44 @@ class Categorizer: def __init__(self): self.options["null_days"] = 4 - def categorize( + def rules( self, - transactions: list[Transaction], + transactions: Transactions, categories: list[Category], tags: list[Tag], ): """Overarching categorization tool - Receives a list of transactions (by ref) and updates their category + Receives a list of transactions (by ref) and updates their category according + to the rules defined for each category Args: transactions (list[Transaction]): uncategorized transactions + categories (list[Category]): available categories + tags (list[Tag]): currently available tags """ self._nullify(transactions) + self._rule_based_categories(transactions, categories) self._rule_based_tags(transactions, tags) - def _nullify(self, transactions: list[Transaction]): + def manual( + self, + transactions: Transactions, + categories: list[Category], + tags: list[Tag], + ): + """Manual categorization input + + Args: + transactions (list[Transaction]): uncategorized transactions + categories (list[Category]): available categories + tags (list[Tag]): currently available tags + """ + self._manual(transactions) + + def _nullify(self, transactions: Transactions): count = 0 matching = [] for transaction in transactions: @@ -66,7 +87,7 @@ class Categorizer: print(f"Nullified {count} transactions") def _rule_based_categories( - self, transactions: list[Transaction], categories: list[Category] + self, transactions: Transactions, categories: list[Category] ): d = {} for category in [c for c in categories if c.rules]: @@ -81,9 +102,17 @@ class Categorizer: continue # passed all conditions, assign category - transaction.category = TransactionCategory( - category.name, CategorySelector(Selector.rules) - ) + if transaction.category: + if ( + input(f"Overwrite {transaction} with {category}? (y/n)") + == "y" + ): + transaction.category.name = category.name + transaction.category.selector.selector = Selector.rules + else: + transaction.category = TransactionCategory( + category.name, CategorySelector(Selector.rules) + ) if rule in d: d[rule] += 1 @@ -93,7 +122,7 @@ class Categorizer: for k, v in d.items(): print(f"{v}: {k}") - def _rule_based_tags(self, transactions: list[Transaction], tags: list[Tag]): + def _rule_based_tags(self, transactions: Transactions, tags: list[Tag]): d = {} for tag in [t for t in tags if t.rules]: for rule in tag.rules: @@ -119,3 +148,20 @@ class Categorizer: for k, v in d.items(): print(f"{v}: {k}") + def _manual(self, transactions: Transactions): + uncategorized = [t for t in transactions if not t.category] + print(f"{len(uncategorized)} transactions left to categorize") + + for transaction in uncategorized: + while True: + category = input(f"{transaction} category: ") + if category == "quit": + return + if not category: + print("{category} doesn't exist") + continue + transaction.category = TransactionCategory( + category, CategorySelector(Selector.manual) + ) + + break diff --git a/pfbudget/core/manager.py b/pfbudget/core/manager.py index 60a5259..9cdc044 100644 --- a/pfbudget/core/manager.py +++ b/pfbudget/core/manager.py @@ -71,10 +71,17 @@ class Manager: case Operation.Categorize: with self.db.session() as session: - uncategorized = session.get(Transaction, ~Transaction.category) + uncategorized = session.get(Transaction, ~Transaction.category.has()) categories = session.get(Category) tags = session.get(Tag) - Categorizer().categorize(uncategorized, categories, tags) + Categorizer().rules(uncategorized, categories, tags) + + case Operation.ManualCategorization: + with self.db.session() as session: + uncategorized = session.get(Transaction, ~Transaction.category.has()) + categories = session.get(Category) + tags = session.get(Tag) + Categorizer().manual(uncategorized, categories, tags) case Operation.BankMod: with self.db.session() as session: diff --git a/pfbudget/db/client.py b/pfbudget/db/client.py index 538e82a..29940d8 100644 --- a/pfbudget/db/client.py +++ b/pfbudget/db/client.py @@ -54,11 +54,11 @@ class DbClient: self.__session.expunge_all() def get(self, type, column=None, values=None): - if column: + if column is not None: if values: stmt = select(type).where(column.in_(values)) else: - stmt = select(type).where(column.has()) + stmt = select(type).where(column) else: stmt = select(type)