Adds --all option to download transactions

There is now the possibility to download the transactions for all banks
in the banks table in the DB.
`NordigenInput` parse method fully functional, and entire chain from
downloading to parsing (simple w/ converter) to writing to DB.
DbTransaction type added __conform__ to simplify writes to DB.
Get bank methods added to both `Manager` and `DatabaseClient`.

Warning: csv parser most likely not working at this point.

Issues #16 and #17
This commit is contained in:
Luís Murta 2022-10-09 18:36:46 +01:00
parent cfcc182f35
commit 9300d42527
Signed by: satprog
GPG Key ID: 169EF1BBD7049F94
7 changed files with 132 additions and 71 deletions

View File

@ -5,7 +5,7 @@ import re
from pfbudget.core.categories import categorize_data from pfbudget.core.categories import categorize_data
from pfbudget.core.manager import Manager from pfbudget.core.manager import Manager
from pfbudget.input.json import JsonParser from pfbudget.input.json import JsonParser
from pfbudget.input.nordigen import Client from pfbudget.input.nordigen import NordigenInput
from pfbudget.db.client import DatabaseClient from pfbudget.db.client import DatabaseClient
import pfbudget.reporting.graph import pfbudget.reporting.graph
import pfbudget.reporting.report import pfbudget.reporting.report
@ -163,24 +163,13 @@ def argparser(manager: Manager) -> argparse.ArgumentParser:
parents=[help], parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter, formatter_class=argparse.ArgumentDefaultsHelpFormatter,
) )
p_register.add_argument("bank", type=str, nargs=1, help="bank option help")
p_register.add_argument( p_register.add_argument(
"bank", "--requisition", type=str, nargs=1, help="requisition option help"
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("--invert", action="store_true")
p_register.add_argument( p_register.add_argument(
"--description", "--description", type=str, nargs="?", help="description option help"
type=str,
nargs="?",
help="description option help"
) )
p_register.set_defaults(func=lambda args: manager.register(vars(args))) p_register.set_defaults(func=lambda args: manager.register(vars(args)))
@ -193,12 +182,7 @@ def argparser(manager: Manager) -> argparse.ArgumentParser:
parents=[help], parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter, formatter_class=argparse.ArgumentDefaultsHelpFormatter,
) )
p_register.add_argument( p_register.add_argument("bank", type=str, nargs=1, help="bank option help")
"bank",
type=str,
nargs=1,
help="bank option help"
)
p_register.set_defaults(func=lambda args: manager.unregister(vars(args))) p_register.set_defaults(func=lambda args: manager.unregister(vars(args)))
""" """
@ -210,7 +194,7 @@ def argparser(manager: Manager) -> argparse.ArgumentParser:
parents=[help], parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter, formatter_class=argparse.ArgumentDefaultsHelpFormatter,
) )
p_nordigen_access.set_defaults(func=lambda args: Client().token()) p_nordigen_access.set_defaults(func=lambda args: NordigenInput(manager).token())
""" """
Access to Nordigen API Access to Nordigen API
@ -224,7 +208,9 @@ def argparser(manager: Manager) -> argparse.ArgumentParser:
p_nordigen_access.add_argument("institution", nargs=1, type=str) p_nordigen_access.add_argument("institution", nargs=1, type=str)
p_nordigen_access.add_argument("country", nargs=1, type=str) p_nordigen_access.add_argument("country", nargs=1, type=str)
p_nordigen_access.set_defaults( p_nordigen_access.set_defaults(
func=lambda args: Client().requisition(args.institution[0], args.country[0]) func=lambda args: NordigenInput().requisition(
args.institution[0], args.country[0]
)
) )
""" """
@ -236,11 +222,15 @@ def argparser(manager: Manager) -> argparse.ArgumentParser:
parents=[help], parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter, formatter_class=argparse.ArgumentDefaultsHelpFormatter,
) )
p_nordigen_download.add_argument("id", nargs=1, type=str) p_nordigen_download.add_argument("--id", nargs="+", type=str)
p_nordigen_download.set_defaults(func=lambda args: Client().download(args.id[0])) p_nordigen_download.add_argument("--name", nargs="+", type=str)
p_nordigen_download.add_argument("--all", action="store_true")
p_nordigen_download.set_defaults(
func=lambda args: manager.parser(NordigenInput(manager, vars(args)))
)
""" """
List available banks to download from List available banks on Nordigen API
""" """
p_nordigen_list = subparsers.add_parser( p_nordigen_list = subparsers.add_parser(
"list", "list",
@ -249,7 +239,7 @@ def argparser(manager: Manager) -> argparse.ArgumentParser:
formatter_class=argparse.ArgumentDefaultsHelpFormatter, formatter_class=argparse.ArgumentDefaultsHelpFormatter,
) )
p_nordigen_list.add_argument("country", nargs=1, type=str) p_nordigen_list.add_argument("country", nargs=1, type=str)
p_nordigen_list.set_defaults(func=lambda args: Client().banks(args.country[0])) p_nordigen_list.set_defaults(func=lambda args: nordigen_banks(manager, args))
""" """
Nordigen JSONs Nordigen JSONs
@ -263,7 +253,9 @@ def argparser(manager: Manager) -> argparse.ArgumentParser:
p_nordigen_json.add_argument("json", nargs=1, type=str) p_nordigen_json.add_argument("json", nargs=1, type=str)
p_nordigen_json.add_argument("bank", nargs=1, type=str) p_nordigen_json.add_argument("bank", nargs=1, type=str)
p_nordigen_json.add_argument("--invert", action=argparse.BooleanOptionalAction) p_nordigen_json.add_argument("--invert", action=argparse.BooleanOptionalAction)
p_nordigen_json.set_defaults(func=lambda args: manager.parser(JsonParser(vars(args)))) p_nordigen_json.set_defaults(
func=lambda args: manager.parser(JsonParser(vars(args)))
)
return parser return parser
@ -318,6 +310,11 @@ def report(args):
pfbudget.reporting.report.detailed(DatabaseClient(args.database), start, end) pfbudget.reporting.report.detailed(DatabaseClient(args.database), start, end)
def nordigen_banks(manager: Manager, args):
input = NordigenInput(manager)
input.list(vars(args)["country"][0])
def run(): def run():
manager = Manager(DEFAULT_DB) manager = Manager(DEFAULT_DB)
args = argparser(manager).parse_args() args = argparser(manager).parse_args()

View File

@ -1,21 +1,21 @@
from pfbudget.input.input import Input from pfbudget.input.input import Input
from pfbudget.input.parsers import parse_data from pfbudget.input.parsers import parse_data
from pfbudget.common.types import Bank, Transaction from pfbudget.common.types import Bank, Banks, Transaction, Transactions
from pfbudget.db.client import DatabaseClient from pfbudget.db.client import DatabaseClient
from pfbudget.utils import convert from pfbudget.utils import convert
class Manager: class Manager:
def __init__(self, db: str): def __init__(self, db: str):
self.db = db self.__db = db
def init(self): def init(self):
client = DatabaseClient(self.db) client = DatabaseClient(self.__db)
client.init() client.init()
def register(self, args: dict): def register(self, args: dict):
print(args) print(args)
client = DatabaseClient(self.db) client = DatabaseClient(self.__db)
client.register_bank( client.register_bank(
Bank( Bank(
( (
@ -30,11 +30,12 @@ class Manager:
) )
def unregister(self, args: dict): def unregister(self, args: dict):
client = DatabaseClient(self.db) client = DatabaseClient(self.__db)
client.unregister_bank(args["bank"][0]) client.unregister_bank(args["bank"][0])
def parser(self, parser: Input): def parser(self, parser: Input):
print(parser.parse()) transactions = parser.parse()
self.add_transactions(transactions)
def parse(self, filename: str, args: dict): def parse(self, filename: str, args: dict):
transactions = parse_data(filename, args) transactions = parse_data(filename, args)
@ -43,6 +44,15 @@ class Manager:
def transactions() -> list[Transaction]: def transactions() -> list[Transaction]:
pass pass
def add_transactions(self, transactions: list[Transaction]): def add_transactions(self, transactions: Transactions):
converted = convert(transactions) client = DatabaseClient(self.__db)
self.__db.insert_transactions(converted) client.insert_transactions([convert(t) for t in transactions])
def get_bank_by(self, key: str, value: str) -> Bank:
client = DatabaseClient(self.__db)
bank = client.get_bank(key, value)
return convert(bank)
def get_banks(self) -> Banks:
client = DatabaseClient(self.__db)
return [convert(bank) for bank in client.get_banks()]

View File

@ -94,7 +94,7 @@ class DatabaseClient:
logger.info(f"Adding {transaction} into {self.db}") logger.info(f"Adding {transaction} into {self.db}")
self.__execute(Q.ADD_TRANSACTION, (transaction.to_list(),)) self.__execute(Q.ADD_TRANSACTION, (transaction.to_list(),))
def insert_transactions(self, transactions: list[list]): def insert_transactions(self, transactions: Q.DbTransactions):
logger.info(f"Adding {len(transactions)} into {self.db}") logger.info(f"Adding {len(transactions)} into {self.db}")
self.__executemany(Q.ADD_TRANSACTION, transactions) self.__executemany(Q.ADD_TRANSACTION, transactions)
@ -197,3 +197,16 @@ class DatabaseClient:
def unregister_bank(self, bank: str): def unregister_bank(self, bank: str):
logger.info(f"Unregistering bank {bank}") logger.info(f"Unregistering bank {bank}")
self.__execute(Q.DELETE_BANK, (bank,)) self.__execute(Q.DELETE_BANK, (bank,))
def get_bank(self, key: str, value: str) -> Q.DbBank | None:
logger.info(f"Get bank with {key} = {value}")
bank = self.__execute(Q.SELECT_BANK.format(key), (value, ))
if bank:
return Q.DbBank(*bank[0])
def get_banks(self) -> Q.DbBanks:
logger.info("Get all banks")
banks = self.__execute(Q.SELECT_BANKS)
if banks:
return [Q.DbBank(*bank) for bank in banks]
return []

View File

@ -1,5 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from decimal import Decimal from decimal import Decimal
import sqlite3
CREATE_TRANSACTIONS_TABLE = """ CREATE_TRANSACTIONS_TABLE = """
CREATE TABLE IF NOT EXISTS "transactions" ( CREATE TABLE IF NOT EXISTS "transactions" (
@ -24,6 +25,16 @@ class DbTransaction:
original: str original: str
comments: str comments: str
def __conform__(self, protocol):
if protocol is sqlite3.PrepareProtocol:
return (
self.date,
self.description,
self.bank,
self.value,
self.category,
)
DbTransactions = list[DbTransaction] DbTransactions = list[DbTransaction]
@ -58,7 +69,6 @@ class DbBank:
DbBanks = list[DbBank] DbBanks = list[DbBank]
ADD_TRANSACTION = """ ADD_TRANSACTION = """
INSERT INTO transactions (date, description, bank, value, category) values (?,?,?,?,?) INSERT INTO transactions (date, description, bank, value, category) values (?,?,?,?,?)
""" """
@ -127,3 +137,14 @@ DELETE_BANK = """
DELETE FROM banks DELETE FROM banks
WHERE name = (?) WHERE name = (?)
""" """
SELECT_BANK = """
SELECT *
FROM banks
WHERE {} = (?)
"""
SELECT_BANKS = """
SELECT *
FROM banks
"""

View File

@ -1,13 +1,21 @@
from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from pfbudget.common.types import Transactions from pfbudget.common.types import Transactions
if TYPE_CHECKING:
from pfbudget.core.manager import Manager
class Input(ABC): class Input(ABC):
@abstractmethod def __init__(self, manager: Manager):
def __init__(self, options: dict): self._manager = manager
self.options = options
@abstractmethod @abstractmethod
def parse(self) -> Transactions: def parse(self) -> Transactions:
return NotImplemented return NotImplemented
@property
def manager(self):
return self._manager

View File

@ -6,8 +6,9 @@ from pfbudget.utils import convert, parse_decimal
class JsonParser(Input): class JsonParser(Input):
def __init__(self, options): def __init__(self, manager, options):
super().__init__(options) super().__init__(manager)
self.options = options
def parse(self) -> Transactions: def parse(self) -> Transactions:
try: try:

View File

@ -5,39 +5,56 @@ import os
import webbrowser import webbrowser
from .input import Input from .input import Input
from pfbudget.common.types import Transactions from pfbudget.common.types import NoBankSelected, Transactions
from pfbudget.utils import convert, parse_decimal from pfbudget.utils import convert
load_dotenv() load_dotenv()
class Client(Input): class NordigenInput(Input):
def __init__(self, options: dict): def __init__(self, manager, options: dict = {}):
super().__init__(options) super().__init__(manager)
self._client = NordigenClient( self._client = NordigenClient(
secret_key=os.environ.get("SECRET_KEY"), secret_key=os.environ.get("SECRET_KEY"),
secret_id=os.environ.get("SECRET_ID"), secret_id=os.environ.get("SECRET_ID"),
) )
self._client.token = self.__token() self.client.token = self.__token()
print(options)
if "all" in options and options["all"]:
self.__banks = self.manager.get_banks()
elif "id" in options and options["id"]:
self.__banks = [
self.manager.get_bank_by("nordigen_id", b) for b in options["id"]
]
elif "name" in options and options["name"]:
self.__banks = [
self.manager.get_bank_by("name", b) for b in options["name"]
]
else:
self.__banks = None
def parse(self) -> Transactions: def parse(self) -> Transactions:
requisition = self._client.requisition.get_requisition_by_id(self.options["id"]) transactions = []
if not self.__banks:
raise NoBankSelected
for acc in requisition["accounts"]: for bank in self.__banks:
account = self._client.account_api(acc) requisition = self.client.requisition.get_requisition_by_id(
d = account.get_transactions()["transactions"] bank.requisition_id
return [ )
convert(
t["bookingDate"], for acc in requisition["accounts"]:
t["remittanceInformationUnstructured"], account = self._client.account_api(acc)
self.options["bank"], d = account.get_transactions()["transactions"]
parse_decimal(t["transactionAmount"]["amount"])
if not self.options["invert"] transactions.extend(
else -parse_decimal(t["transactionAmount"]["amount"]), [convert(t, bank.name, bank.invert) for t in d["booked"]]
) )
for t in d["booked"]
] return transactions
def token(self): def token(self):
token = self._client.generate_token() token = self._client.generate_token()
@ -48,13 +65,7 @@ class Client(Input):
link, _ = self.__requisition_id(institution, country) link, _ = self.__requisition_id(institution, country)
webbrowser.open(link) webbrowser.open(link)
def download(self, id: str): def list(self, country: str):
if len(id) > 0:
return self.parse(id)
else:
print("you forgot the req id")
def banks(self, country: str):
print(self._client.institution.get_institutions(country)) print(self._client.institution.get_institutions(country))
@property @property