budget/pfbudget/core/input/parsers.py
Luís Murta 4b5773aa13
Restructes project and adds basic Nordigen API
To improve the code organization, additional subfolders were added into
the pfbudget module.

The barebones of renewing the token and downloading transactions
directly from the banks through the Nordigen API were also added.

Issues #13, #15
2022-10-06 22:22:53 +01:00

177 lines
5.3 KiB
Python

from collections import namedtuple
from decimal import Decimal
from importlib import import_module
import datetime as dt
import yaml
from pfbudget.core.transactions import Transaction
from pfbudget.utils import utils
Index = namedtuple(
"Index", ["date", "text", "value", "negate"], defaults=[-1, -1, -1, False]
)
Options = namedtuple(
"Options",
[
"encoding",
"separator",
"date_fmt",
"start",
"end",
"debit",
"credit",
"additional_parser",
"category",
"VISA",
"MasterCard",
"AmericanExpress",
],
defaults=[
"",
"",
"",
1,
None,
Index(),
Index(),
False,
None,
None,
None,
None,
],
)
def parse_data(db: DBManager, filename: str, args: dict) -> None:
cfg: dict = yaml.safe_load(open("parsers.yaml"))
assert (
"Banks" in cfg
), "parsers.yaml is missing the Banks section with the list of available banks"
if not args["bank"]:
bank, creditcard = utils.find_credit_institution(
filename, cfg.get("Banks"), cfg.get("CreditCards")
)
else:
bank = args["bank"][0]
creditcard = None if not args["creditcard"] else args["creditcard"][0]
if not creditcard:
options: dict = cfg[bank]
else:
options: dict = cfg[bank][creditcard]
bank += creditcard
if args["category"]:
options["category"] = args["category"][0]
if options.get("additional_parser"):
parser = getattr(import_module("pfbudget.parsers"), bank)
transactions = parser(filename, bank, options).parse()
else:
transactions = Parser(filename, bank, options).parse()
db.insert_transactions(transactions)
class Parser:
def __init__(self, filename: str, bank: str, options: dict):
self.filename = filename
self.bank = bank
if debit := options.get("debit", None):
options["debit"] = Index(**debit)
if credit := options.get("credit", None):
options["credit"] = Index(**credit)
self.options = Options(**options)
def func(self, transaction: Transaction):
pass
def parse(self) -> list[Transaction]:
transactions = [
Parser.transaction(line, self.bank, self.options, self.func)
for line in list(open(self.filename, encoding=self.options.encoding))[
self.options.start - 1 : self.options.end
]
if len(line) > 2
]
return transactions
@staticmethod
def index(line: list, options: Options) -> Index:
if options.debit.date != -1 and options.credit.date != -1:
if options.debit.value != options.credit.value:
if line[options.debit.value]:
index = options.debit
elif line[options.credit.value]:
index = options.credit
elif options.debit.date != options.credit.date:
negate = 1 if (options.debit.negate or options.credit.negate) else -1
if (negate * utils.parse_decimal(line[options.debit.value])) < 0:
index = options.debit
else:
index = options.credit
elif options.debit.text != options.credit.text:
if line[options.debit.text]:
index = options.debit
elif line[options.credit.text]:
index = options.credit
else:
raise IndexError("Debit and credit indexes are equal")
elif options.debit.date != -1:
index = options.debit
elif options.credit.date != -1:
index = options.credit
else:
raise IndexError("No debit not credit indexes available")
return index
@staticmethod
def transaction(line: str, bank: str, options: Options, func) -> Transaction:
line = line.rstrip().split(options.separator)
index = Parser.index(line, options)
date = (
dt.datetime.strptime(line[index.date].strip(), options.date_fmt)
.date()
.isoformat()
)
text = line[index.text]
value = utils.parse_decimal(line[index.value])
if index.negate:
value = -value
if options.category:
category = line[options.category]
transaction = Transaction(date, text, bank, value, category)
else:
transaction = Transaction(date, text, bank, value, options.category)
if options.additional_parser:
func(transaction)
return transaction
class Bank1(Parser):
def __init__(self, filename: str, bank: str, options: dict):
super().__init__(filename, bank, options)
self.transfers = []
self.transaction_cost = -Decimal("1")
def func(self, transaction: Transaction):
if "transf" in transaction.description.lower() and transaction.value < 0:
transaction.value -= self.transaction_cost
self.transfers.append(transaction.date)
def parse(self) -> list:
transactions = super().parse()
for date in self.transfers:
transactions.append(
Transaction(date, "Transaction cost", self.bank, self.transaction_cost)
)
return transactions