From c42a399d3d08e49620234aaf7025909568615ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Murta?= Date: Tue, 10 Jan 2023 23:45:09 +0000 Subject: [PATCH] Adds the import operation and a timer to the categorization. We can now import transactions from a csv file, and later automatically categorize them all. --- pfbudget/__main__.py | 6 ++++++ pfbudget/cli/runnable.py | 4 ++++ pfbudget/common/types.py | 1 + pfbudget/core/categorizer.py | 9 ++++++++- pfbudget/core/manager.py | 22 ++++++++++++++++++++-- pfbudget/output/csv.py | 22 ++++++++++++++++++++-- 6 files changed, 59 insertions(+), 5 deletions(-) diff --git a/pfbudget/__main__.py b/pfbudget/__main__.py index fa5eb31..c732656 100644 --- a/pfbudget/__main__.py +++ b/pfbudget/__main__.py @@ -242,4 +242,10 @@ if __name__ == "__main__": params.append(args["banks"]) params.append(args["file"][0]) + case pfbudget.Operation.Import: + keys = {"file"} + assert args.keys() >= keys, f"missing {args.keys() - keys}" + + params = args["file"] + pfbudget.Manager(db, verbosity).action(op, params) diff --git a/pfbudget/cli/runnable.py b/pfbudget/cli/runnable.py index b367ad2..5524c3f 100644 --- a/pfbudget/cli/runnable.py +++ b/pfbudget/cli/runnable.py @@ -83,6 +83,10 @@ def argparser() -> argparse.ArgumentParser: export_banks.add_argument("--all", action="store_true") export_banks.add_argument("--banks", nargs="+", type=str) + pimport = subparsers.add_parser("import") + pimport.set_defaults(op=Operation.Import) + pimport.add_argument("file", nargs=1, type=str) + # Parse from .csv parse = subparsers.add_parser("parse") parse.set_defaults(op=Operation.Parse) diff --git a/pfbudget/common/types.py b/pfbudget/common/types.py index 2f98d41..d5eba7d 100644 --- a/pfbudget/common/types.py +++ b/pfbudget/common/types.py @@ -36,6 +36,7 @@ class Operation(Enum): NordigenDel = auto() NordigenCountryBanks = auto() Export = auto() + Import = auto() class TransactionError(Exception): diff --git a/pfbudget/core/categorizer.py b/pfbudget/core/categorizer.py index d2918e3..0cfc5af 100644 --- a/pfbudget/core/categorizer.py +++ b/pfbudget/core/categorizer.py @@ -8,6 +8,7 @@ from pfbudget.db.model import ( TransactionTag, ) +from codetiming import Timer from datetime import timedelta Transactions = list[Transaction] @@ -56,6 +57,7 @@ class Categorizer: """ self._manual(transactions) + @Timer(name="nullify") def _nullify(self, transactions: Transactions): count = 0 matching = [] @@ -86,6 +88,7 @@ class Categorizer: print(f"Nullified {count} transactions") + @Timer(name="categoryrules") def _rule_based_categories( self, transactions: Transactions, categories: list[Category] ): @@ -102,7 +105,10 @@ class Categorizer: continue # passed all conditions, assign category - if transaction.category: + if ( + transaction.category + and transaction.category.name == category.name + ): if ( input(f"Overwrite {transaction} with {category}? (y/n)") == "y" @@ -122,6 +128,7 @@ class Categorizer: for k, v in d.items(): print(f"{v}: {k}") + @Timer(name="tagrules") def _rule_based_tags(self, transactions: Transactions, tags: list[Tag]): d = {} for tag in [t for t in tags if t.rules]: diff --git a/pfbudget/core/manager.py b/pfbudget/core/manager.py index 9cdc044..0801e5e 100644 --- a/pfbudget/core/manager.py +++ b/pfbudget/core/manager.py @@ -71,14 +71,18 @@ class Manager: case Operation.Categorize: with self.db.session() as session: - uncategorized = session.get(Transaction, ~Transaction.category.has()) + uncategorized = session.get( + Transaction, ~Transaction.category.has() + ) categories = session.get(Category) tags = session.get(Tag) Categorizer().rules(uncategorized, categories, tags) case Operation.ManualCategorization: with self.db.session() as session: - uncategorized = session.get(Transaction, ~Transaction.category.has()) + uncategorized = session.get( + Transaction, ~Transaction.category.has() + ) categories = session.get(Category) tags = session.get(Tag) Categorizer().manual(uncategorized, categories, tags) @@ -178,6 +182,20 @@ class Manager: csvwriter: Output = CSV(params[-1]) csvwriter.report(transactions) + case Operation.Import: + csvwriter: Output = CSV(params[0]) # Output is strange here + transactions = csvwriter.load() + + if ( + len(transactions) > 0 + and input( + f"{transactions[:5]}\nDoes the import seem correct? (y/n)" + ) + == "y" + ): + with self.db.session() as session: + session.add(transactions) + # def init(self): # client = DatabaseClient(self.__db) # client.init() diff --git a/pfbudget/output/csv.py b/pfbudget/output/csv.py index 55135e8..77b1191 100644 --- a/pfbudget/output/csv.py +++ b/pfbudget/output/csv.py @@ -1,6 +1,10 @@ -from csv import writer +from csv import DictReader, writer -from pfbudget.db.model import Transaction +from pfbudget.db.model import ( + BankTransaction, + MoneyTransaction, + Transaction, +) from .output import Output @@ -9,6 +13,20 @@ class CSV(Output): def __init__(self, filename: str): self.fn = filename + def load(self) -> list[Transaction]: + with open(self.fn, "r", newline="") as f: + r = DictReader(f) + return [ + BankTransaction( + row["date"], row["description"], row["amount"], False, row["bank"] + ) + if row["bank"] + else MoneyTransaction( + row["date"], row["description"], False, row["amount"] + ) + for row in r + ] + def report(self, transactions: list[Transaction]): with open(self.fn, "w", newline="") as f: w = writer(f, delimiter="\t")