From 0a42db899597ae5a5241fa39d09b34cae9737c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Murta?= Date: Thu, 29 Sep 2022 23:04:10 +0100 Subject: [PATCH] Persists banks information and requisition ID This patch saves the bank information in the DB, in a new table. It also adds two new CLI commands, register/unregister, so enter the banking information. (This should later be done internally). It also adds new types alias for the DB transaction type and new converters. Input `transactions` method renamed to `parse`. Issue #18 --- pfbudget/cli/runnable.py | 47 +++++++++++++++++++++++++++++++++ pfbudget/core/input/input.py | 2 +- pfbudget/core/input/json.py | 36 +++++++++++++------------ pfbudget/core/input/nordigen.py | 4 +-- pfbudget/core/manager.py | 28 +++++++++++++++++--- pfbudget/db/client.py | 13 +++++++++ pfbudget/db/schema.py | 22 +++++++++++++-- pfbudget/utils/converters.py | 30 ++++++++++++++++----- 8 files changed, 150 insertions(+), 32 deletions(-) diff --git a/pfbudget/cli/runnable.py b/pfbudget/cli/runnable.py index 3618a9f..3aa4d49 100644 --- a/pfbudget/cli/runnable.py +++ b/pfbudget/cli/runnable.py @@ -155,6 +155,53 @@ def argparser(manager: Manager) -> argparse.ArgumentParser: ) p_report.set_defaults(func=report) + """ + Register bank + """ + p_register = subparsers.add_parser( + "register", + description="Register a bank", + parents=[help], + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + p_register.add_argument( + "bank", + type=str, + nargs=1, + help="bank option help" + ) + p_register.add_argument( + "--requisition", + type=str, + nargs=1, + help="requisition option help" + ) + p_register.add_argument("--invert", action="store_true") + p_register.add_argument( + "--description", + type=str, + nargs="?", + help="description option help" + ) + p_register.set_defaults(func=lambda args: manager.register(vars(args))) + + """ + Unregister bank + """ + p_register = subparsers.add_parser( + "unregister", + description="Unregister a bank", + parents=[help], + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + p_register.add_argument( + "bank", + type=str, + nargs=1, + help="bank option help" + ) + p_register.set_defaults(func=lambda args: manager.unregister(vars(args))) + """ Nordigen API """ diff --git a/pfbudget/core/input/input.py b/pfbudget/core/input/input.py index debd3bb..c84501d 100644 --- a/pfbudget/core/input/input.py +++ b/pfbudget/core/input/input.py @@ -9,5 +9,5 @@ class Input(ABC): self.options = options @abstractmethod - def transactions(self) -> Transactions: + def parse(self) -> Transactions: return NotImplemented diff --git a/pfbudget/core/input/json.py b/pfbudget/core/input/json.py index bc5235f..fd1ea8e 100644 --- a/pfbudget/core/input/json.py +++ b/pfbudget/core/input/json.py @@ -1,28 +1,30 @@ import json +from pfbudget.core.input.input import Input from pfbudget.core.transactions import Transactions from pfbudget.utils.converters import convert from pfbudget.utils.utils import parse_decimal -from .input import Input - class JsonParser(Input): def __init__(self, options): super().__init__(options) - def transactions(self) -> Transactions: - with open(self.options["json"][0], "r") as f: - return [ - convert( - [ - t["bookingDate"], - t["remittanceInformationUnstructured"], - self.options["bank"][0], - parse_decimal(t["transactionAmount"]["amount"]) - if not self.options["invert"] - else -parse_decimal(t["transactionAmount"]["amount"]), - ], - ) - for t in json.load(f)["transactions"]["booked"] - ] + def parse(self) -> Transactions: + try: + with open(self.options["json"][0], "r") as f: + return [ + convert( + [ + t["bookingDate"], + t["remittanceInformationUnstructured"], + self.options["bank"][0], + parse_decimal(t["transactionAmount"]["amount"]) + if not self.options["invert"] + else -parse_decimal(t["transactionAmount"]["amount"]), + ], + ) + for t in json.load(f)["transactions"]["booked"] + ] + except KeyError: + print("No json file defined") diff --git a/pfbudget/core/input/nordigen.py b/pfbudget/core/input/nordigen.py index 1afa470..ca2ff6d 100644 --- a/pfbudget/core/input/nordigen.py +++ b/pfbudget/core/input/nordigen.py @@ -22,7 +22,7 @@ class Client(Input): self._client.token = self.__token() - def transactions(self) -> Transactions: + def parse(self) -> Transactions: requisition = self._client.requisition.get_requisition_by_id(self.options["id"]) for acc in requisition["accounts"]: @@ -51,7 +51,7 @@ class Client(Input): def download(self, id: str): if len(id) > 0: - return self.transactions(id) + return self.parse(id) else: print("you forgot the req id") diff --git a/pfbudget/core/manager.py b/pfbudget/core/manager.py index a6458db..9b9d6e4 100644 --- a/pfbudget/core/manager.py +++ b/pfbudget/core/manager.py @@ -2,18 +2,40 @@ from pfbudget.core.input.input import Input from pfbudget.core.input.parsers import parse_data from pfbudget.core.transactions import Transaction from pfbudget.db.client import DatabaseClient +from pfbudget.db.schema import Bank from pfbudget.utils.converters import convert class Manager: def __init__(self, db: str): - self.__db = DatabaseClient(db) + self.db = db def init(self): - self.__db.init() + client = DatabaseClient(self.db) + client.init() + + def register(self, args: dict): + print(args) + client = DatabaseClient(self.db) + client.register_bank( + Bank( + ( + args["bank"][0], + args["requisition"][0] + if args["requisition"] + else args["requisition"], + args["invert"], + args["description"], + ) + ) + ) + + def unregister(self, args: dict): + client = DatabaseClient(self.db) + client.unregister_bank(args["bank"][0]) def parser(self, parser: Input): - print(parser.transactions()) + print(parser.parse()) def parse(self, filename: str, args: dict): transactions = parse_data(filename, args) diff --git a/pfbudget/db/client.py b/pfbudget/db/client.py index d6084bb..9259df7 100644 --- a/pfbudget/db/client.py +++ b/pfbudget/db/client.py @@ -77,9 +77,12 @@ class DatabaseClient: ( ("transactions", Q.CREATE_TRANSACTIONS_TABLE), ("backups", Q.CREATE_BACKUPS_TABLE), + ("banks", Q.CREATE_BANKS_TABLE), ) ) + """Transaction table methods""" + def select_all(self) -> list[Transaction] | None: logger.info(f"Reading all transactions from {self.db}") transactions = self.__execute("SELECT * FROM transactions") @@ -184,3 +187,13 @@ class DatabaseClient: dir.mkdir() with open(dir / filename, "w", newline="") as f: csv.writer(f, delimiter="\t").writerows(transactions) + + """Banks table methods""" + + def register_bank(self, bank: Q.Bank): + logger.info(f"Registering bank {bank[0]} with req_id={bank[1]}") + self.__execute(Q.ADD_BANK, (bank[0], bank[1], bank[2], bank[3])) + + def unregister_bank(self, bank: str): + logger.info(f"Unregistering bank {bank}") + self.__execute(Q.DELETE_BANK, (bank,)) diff --git a/pfbudget/db/schema.py b/pfbudget/db/schema.py index 80679b7..3c97056 100644 --- a/pfbudget/db/schema.py +++ b/pfbudget/db/schema.py @@ -1,3 +1,5 @@ +from decimal import Decimal + CREATE_TRANSACTIONS_TABLE = """ CREATE TABLE IF NOT EXISTS "transactions" ( "date" TEXT NOT NULL, @@ -10,6 +12,9 @@ CREATE TABLE IF NOT EXISTS "transactions" ( ); """ +DbTransaction = tuple[str, str | None, str, Decimal, str | None, str | None, str | None] +DbTransactions = list[DbTransaction] + CREATE_BACKUPS_TABLE = """ CREATE TABLE IF NOT EXISTS backups ( datetime TEXT NOT NULL, @@ -18,12 +23,16 @@ CREATE TABLE IF NOT EXISTS backups ( """ CREATE_BANKS_TABLE = """ -CREATE TABLE banks ( +CREATE TABLE IF NOT EXISTS banks ( name TEXT NOT NULL PRIMARY KEY, - url TEXT + requisition TEXT, + invert INTEGER, + description TEXT ) """ +Bank = tuple[str, str, bool] + ADD_TRANSACTION = """ INSERT INTO transactions (date, description, bank, value, category) values (?,?,?,?,?) """ @@ -83,3 +92,12 @@ WHERE date BETWEEN (?) AND (?) AND category NOT IN {} ORDER BY date ASC """ + +ADD_BANK = """ +INSERT INTO banks (name, requisition, invert, description) values (?,?,?,?) +""" + +DELETE_BANK = """ +DELETE FROM banks +WHERE name = (?) +""" diff --git a/pfbudget/utils/converters.py b/pfbudget/utils/converters.py index 75780dd..9bf1ecb 100644 --- a/pfbudget/utils/converters.py +++ b/pfbudget/utils/converters.py @@ -1,6 +1,7 @@ from functools import singledispatch from pfbudget.core.transactions import Transaction, TransactionError, Transactions +from pfbudget.db.schema import DbTransaction, DbTransactions @singledispatch @@ -9,20 +10,35 @@ def convert(t): @convert.register -def _(t: Transaction) -> list: +def _(t: Transaction) -> DbTransaction: return (t.date, t.description, t.bank, t.value, t.category) -@convert.register -def _(t: list) -> Transaction: +def convert_dbtransaction(db) -> Transaction: try: - return Transaction(t) + return Transaction(db) except TransactionError: - print(f"{t} is in the wrong format") + print(f"{db} is in the wrong format") -def convert_transactions(transactions) -> list[list]: - return [convert(c) for c in transactions] +convert.register(type(DbTransaction), convert_dbtransaction) + + +def convert_transactions(ts: Transactions) -> DbTransactions: + try: + return [convert(t) for t in ts] + except TransactionError: + print(f"{ts} is in the wrong format") convert.register(type(Transactions), convert_transactions) + + +def convert_dbtransactions(ts: DbTransactions) -> Transactions: + try: + return [convert(t) for t in ts] + except TransactionError: + print(f"{ts} is in the wrong format") + + +convert.register(type(DbTransactions), convert_dbtransactions)