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
This commit is contained in:
Luís Murta 2022-09-23 20:34:45 +01:00
parent 2af9b6ebc6
commit 4b5773aa13
Signed by: satprog
GPG Key ID: 169EF1BBD7049F94
16 changed files with 184 additions and 59 deletions

View File

@ -2,6 +2,6 @@ __all__ = ["run", "parse_data", "categorize_data"]
__author__ = "Luís Murta" __author__ = "Luís Murta"
__version__ = "0.1" __version__ = "0.1"
from .categories import categorize_data from pfbudget.core.categories import categorize_data
from .parsers import parse_data from pfbudget.core.input.parsers import parse_data
from .runnable import run from pfbudget.cli.runnable import run

4
pfbudget/__main__.py Normal file
View File

@ -0,0 +1,4 @@
from pfbudget.cli.runnable import run
if __name__ == "__main__":
run()

0
pfbudget/cli/__init__.py Normal file
View File

View File

@ -2,13 +2,16 @@ from pathlib import Path
import argparse import argparse
import re import re
from .categories import categorize_data from pfbudget.core.categories import categorize_data
from .database import DBManager from pfbudget.core.input.parsers import parse_data
from .parsers import parse_data from pfbudget.db.client import DBManager
import pfbudget.graph import pfbudget.reporting.graph
import pfbudget.report import pfbudget.reporting.report
import pfbudget.utils import pfbudget.utils
from pfbudget.core.input.nordigen import Client
DEFAULT_DB = "data.db" DEFAULT_DB = "data.db"
@ -150,6 +153,56 @@ def argparser() -> argparse.ArgumentParser:
) )
p_report.set_defaults(func=report) p_report.set_defaults(func=report)
"""
Init Nordigen session: get new access token
"""
p_nordigen_access = subparsers.add_parser(
"init",
description="Get new access token",
parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
p_nordigen_access.set_defaults(func=lambda args: Client().token())
"""
Access to Nordigen API
"""
p_nordigen_access = subparsers.add_parser(
"renew",
description="Renew the requisition ID",
parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
p_nordigen_access.add_argument("institution", nargs=1, type=str)
p_nordigen_access.add_argument("country", nargs=1, type=str)
p_nordigen_access.set_defaults(
func=lambda args: Client().requisition(args.institution[0], args.country[0])
)
"""
Downloading through Nordigen API
"""
p_nordigen_download = subparsers.add_parser(
"download",
description="Downloads transactions using Nordigen API",
parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
p_nordigen_download.add_argument("id", nargs=1, type=str)
p_nordigen_download.set_defaults(func=lambda args: Client().download(args.id[0]))
"""
List available banks to download from
"""
p_nordigen_list = subparsers.add_parser(
"list",
description="Lists banks in {country}",
parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
p_nordigen_list.add_argument("country", nargs=1, type=str)
p_nordigen_list.set_defaults(func=lambda args: Client().banks(args.country[0]))
return parser return parser
@ -178,11 +231,11 @@ def graph(args):
""" """
start, end = pfbudget.utils.parse_args_period(args) start, end = pfbudget.utils.parse_args_period(args)
if args.option == "monthly": if args.option == "monthly":
pfbudget.graph.monthly(DBManager(args.database), vars(args), start, end) pfbudget.reporting.graph.monthly(DBManager(args.database), vars(args), start, end)
elif args.option == "discrete": elif args.option == "discrete":
pfbudget.graph.discrete(DBManager(args.database), vars(args), start, end) pfbudget.reporting.graph.discrete(DBManager(args.database), vars(args), start, end)
elif args.option == "networth": elif args.option == "networth":
pfbudget.graph.networth(DBManager(args.database), vars(args), start, end) pfbudget.reporting.graph.networth(DBManager(args.database), vars(args), start, end)
def report(args): def report(args):
@ -193,9 +246,9 @@ def report(args):
""" """
start, end = pfbudget.utils.parse_args_period(args) start, end = pfbudget.utils.parse_args_period(args)
if args.option == "net": if args.option == "net":
pfbudget.report.net(DBManager(args.database), start, end) pfbudget.reporting.report.net(DBManager(args.database), start, end)
elif args.option == "detailed": elif args.option == "detailed":
pfbudget.report.detailed(DBManager(args.database), start, end) pfbudget.reporting.report.detailed(DBManager(args.database), start, end)
def run(): def run():

View File

View File

@ -8,8 +8,8 @@ import yaml
if TYPE_CHECKING: if TYPE_CHECKING:
from pfbudget.database import DBManager from pfbudget.db.manager import DBManager
from pfbudget.transactions import Transaction from pfbudget.core.transactions import Transaction
Options = namedtuple( Options = namedtuple(

View File

View File

@ -0,0 +1,74 @@
from dotenv import load_dotenv
from json import dump
from nordigen import NordigenClient
from uuid import uuid4
import os
import webbrowser
load_dotenv()
class Client:
def __init__(self):
self._client = NordigenClient(
secret_key=os.environ.get("SECRET_KEY"),
secret_id=os.environ.get("SECRET_ID"),
)
self._client.token = self.__token()
def token(self):
token = self._client.generate_token()
print(f"New access token: {token}")
return token
def requisition(self, institution: str, country: str = "PT"):
link, _ = self.__requisition_id(institution, country)
webbrowser.open(link)
def download(self, id: str):
if len(id) > 0:
return self.transactions(id)
else:
print("you forgot the req id")
def transactions(self, id):
requisition = self._client.requisition.get_requisition_by_id(id)
# transactions_list = []
for acc in requisition["accounts"]:
account = self._client.account_api(acc)
print(account.get_metadata())
with open("cetelem.json", "w") as f:
dump(account.get_transactions(), f, indent=4)
# print(dumps(account.get_transactions(), indent=4))
# print(transactions_list)
def banks(self, country: str):
print(self._client.institution.get_institutions(country))
@property
def client(self):
return self._client
def __token(self):
if token := os.environ.get("TOKEN"):
return token
else:
token = self._client.generate_token()
print(f"New access token: {token}")
return token
def __requisition_id(self, i: str, c: str):
id = self._client.institution.get_institution_id_by_name(
country=c, institution=i
)
init = self._client.initialize_session(
redirect_uri="https://murta.dev",
institution_id=id,
reference_id=str(uuid4()),
)
print(f"{i}({c}) link: {init.link} and requisition ID: {init.requisition_id}")
return (init.link, init.requisition_id)

View File

@ -1,16 +1,11 @@
from __future__ import annotations
from collections import namedtuple from collections import namedtuple
from decimal import Decimal from decimal import Decimal
from importlib import import_module from importlib import import_module
from typing import TYPE_CHECKING
import datetime as dt import datetime as dt
import yaml import yaml
from .transactions import Transaction from pfbudget.core.transactions import Transaction
from . import utils from pfbudget.utils import utils
if TYPE_CHECKING:
from .database import DBManager
Index = namedtuple( Index = namedtuple(
"Index", ["date", "text", "value", "negate"], defaults=[-1, -1, -1, False] "Index", ["date", "text", "value", "negate"], defaults=[-1, -1, -1, False]

View File

@ -7,7 +7,7 @@ import logging.config
import pathlib import pathlib
import sqlite3 import sqlite3
from .transactions import Transaction from ..core.transactions import Transaction
if not pathlib.Path("logs").is_dir(): if not pathlib.Path("logs").is_dir():

View File

View File

@ -5,14 +5,14 @@ from typing import TYPE_CHECKING
import datetime as dt import datetime as dt
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pfbudget.categories import pfbudget.core.categories
if TYPE_CHECKING: if TYPE_CHECKING:
from pfbudget.database import DBManager from pfbudget.db.manager import DBManager
groups = pfbudget.categories.cfg["Groups"] groups = pfbudget.core.categories.cfg["Groups"]
def monthly( def monthly(
@ -33,7 +33,7 @@ def monthly(
<= month <= month
+ dt.timedelta(days=monthrange(month.year, month.month)[1] - 1) + dt.timedelta(days=monthrange(month.year, month.month)[1] - 1)
) )
for group, categories in pfbudget.categories.groups.items() for group, categories in pfbudget.core.categories.groups.items()
}, },
) )
for month in [ for month in [
@ -68,21 +68,21 @@ def monthly(
list(rrule(MONTHLY, dtstart=start.replace(day=1), until=end.replace(day=1))), list(rrule(MONTHLY, dtstart=start.replace(day=1), until=end.replace(day=1))),
[ [
[-groups[group] for _, groups in monthly_transactions] [-groups[group] for _, groups in monthly_transactions]
for group in pfbudget.categories.groups for group in pfbudget.core.categories.groups
if group != "income-fixed" if group != "income-fixed"
and group != "income-extra" and group != "income-extra"
and group != "investment" and group != "investment"
], ],
labels=[ labels=[
group group
for group in pfbudget.categories.groups for group in pfbudget.core.categories.groups
if group != "income-fixed" if group != "income-fixed"
and group != "income-extra" and group != "income-extra"
and group != "investment" and group != "investment"
], ],
colors=[ colors=[
groups.get(group, {"color": "gray"})["color"] groups.get(group, {"color": "gray"})["color"]
for group in pfbudget.categories.groups for group in pfbudget.core.categories.groups
if group != "income-fixed" if group != "income-fixed"
and group != "income-extra" and group != "income-extra"
and group != "investment" and group != "investment"
@ -113,7 +113,7 @@ def discrete(
<= month <= month
+ dt.timedelta(days=monthrange(month.year, month.month)[1] - 1) + dt.timedelta(days=monthrange(month.year, month.month)[1] - 1)
) )
for category in pfbudget.categories.categories for category in pfbudget.core.categories.categories
}, },
) )
for month in [ for month in [
@ -131,8 +131,8 @@ def discrete(
sum( sum(
value value
for category, value in categories.items() for category, value in categories.items()
if category in pfbudget.categories.groups["income-fixed"] if category in pfbudget.core.categories.groups["income-fixed"]
or category in pfbudget.categories.groups["income-extra"] or category in pfbudget.core.categories.groups["income-extra"]
) )
for _, categories in monthly_transactions for _, categories in monthly_transactions
], ],
@ -145,7 +145,7 @@ def discrete(
sum( sum(
value value
for category, value in categories.items() for category, value in categories.items()
if category in pfbudget.categories.groups["income-fixed"] if category in pfbudget.core.categories.groups["income-fixed"]
) )
for _, categories in monthly_transactions for _, categories in monthly_transactions
], ],
@ -156,18 +156,18 @@ def discrete(
list(rrule(MONTHLY, dtstart=start.replace(day=1), until=end.replace(day=1))), list(rrule(MONTHLY, dtstart=start.replace(day=1), until=end.replace(day=1))),
[ [
[-categories[category] for _, categories in monthly_transactions] [-categories[category] for _, categories in monthly_transactions]
for category in pfbudget.categories.categories for category in pfbudget.core.categories.categories
if category not in pfbudget.categories.groups["income-fixed"] if category not in pfbudget.core.categories.groups["income-fixed"]
and category not in pfbudget.categories.groups["income-extra"] and category not in pfbudget.core.categories.groups["income-extra"]
and category not in pfbudget.categories.groups["investment"] and category not in pfbudget.core.categories.groups["investment"]
and category != "Null" and category != "Null"
], ],
labels=[ labels=[
category category
for category in pfbudget.categories.categories for category in pfbudget.core.categories.categories
if category not in pfbudget.categories.groups["income-fixed"] if category not in pfbudget.core.categories.groups["income-fixed"]
and category not in pfbudget.categories.groups["income-extra"] and category not in pfbudget.core.categories.groups["income-extra"]
and category not in pfbudget.categories.groups["investment"] and category not in pfbudget.core.categories.groups["investment"]
and category != "Null" and category != "Null"
], ],
) )
@ -193,11 +193,12 @@ def networth(
transaction.value transaction.value
for transaction in transactions for transaction in transactions
if transaction.original != "No" if transaction.original != "No"
and transaction.category not in pfbudget.categories.groups["investment"] and transaction.category not in pfbudget.core.categories.groups["investment"]
and month and month
<= transaction.date <= transaction.date
<= month + dt.timedelta(days=monthrange(month.year, month.month)[1] - 1) <= month + dt.timedelta(days=monthrange(month.year, month.month)[1] - 1)
) + accum )
+ accum,
) )
for month in [ for month in [
month.date() month.date()
@ -210,10 +211,8 @@ def networth(
plt.figure(tight_layout=True) plt.figure(tight_layout=True)
plt.plot( plt.plot(
list(rrule(MONTHLY, dtstart=start.replace(day=1), until=end.replace(day=1))), list(rrule(MONTHLY, dtstart=start.replace(day=1), until=end.replace(day=1))),
[ [value for _, value in monthly_networth],
value for _, value in monthly_networth label="Total networth",
],
label="Total networth"
) )
plt.grid() plt.grid()
plt.legend(loc="upper left") plt.legend(loc="upper left")

View File

@ -3,10 +3,10 @@ from dateutil.rrule import rrule, YEARLY
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import datetime as dt import datetime as dt
import pfbudget.categories import pfbudget.core.categories
if TYPE_CHECKING: if TYPE_CHECKING:
from pfbudget.database import DBManager from pfbudget.db.manager import DBManager
def net(db: DBManager, start: dt.date = dt.date.min, end: dt.date = dt.date.max): def net(db: DBManager, start: dt.date = dt.date.min, end: dt.date = dt.date.max):
@ -23,7 +23,7 @@ def net(db: DBManager, start: dt.date = dt.date.min, end: dt.date = dt.date.max)
if transaction.category in categories if transaction.category in categories
and year <= transaction.date <= year.replace(month=12, day=31) and year <= transaction.date <= year.replace(month=12, day=31)
) )
for group, categories in pfbudget.categories.groups.items() for group, categories in pfbudget.core.categories.groups.items()
}, },
) )
for year in [ for year in [
@ -62,6 +62,7 @@ def net(db: DBManager, start: dt.date = dt.date.min, end: dt.date = dt.date.max)
print(f"Invested: {investments:.2f}\n") print(f"Invested: {investments:.2f}\n")
def detailed(db: DBManager, start: dt.date = dt.date.min, end: dt.date = dt.date.max): def detailed(db: DBManager, start: dt.date = dt.date.min, end: dt.date = dt.date.max):
transactions = db.get_daterange(start, end) transactions = db.get_daterange(start, end)
start, end = transactions[0].date, transactions[-1].date start, end = transactions[0].date, transactions[-1].date
@ -76,7 +77,7 @@ def detailed(db: DBManager, start: dt.date = dt.date.min, end: dt.date = dt.date
if transaction.category == category if transaction.category == category
and year <= transaction.date <= year.replace(month=12, day=31) and year <= transaction.date <= year.replace(month=12, day=31)
) )
for category in pfbudget.categories.categories for category in pfbudget.core.categories.categories
}, },
) )
for year in [ for year in [
@ -93,23 +94,23 @@ def detailed(db: DBManager, start: dt.date = dt.date.min, end: dt.date = dt.date
income = sum( income = sum(
sum sum
for category, sum in categories.items() for category, sum in categories.items()
if category in pfbudget.categories.groups["income-fixed"] if category in pfbudget.core.categories.groups["income-fixed"]
or category in pfbudget.categories.groups["income-extra"] or category in pfbudget.core.categories.groups["income-extra"]
) )
print(f"Income: {income:.2f}\n") print(f"Income: {income:.2f}\n")
investments = -sum( investments = -sum(
sum sum
for category, sum in categories.items() for category, sum in categories.items()
if category in pfbudget.categories.groups["investment"] if category in pfbudget.core.categories.groups["investment"]
) )
expenses = 0 expenses = 0
for category, value in categories.items(): for category, value in categories.items():
if ( if (
category not in pfbudget.categories.groups["income-fixed"] category not in pfbudget.core.categories.groups["income-fixed"]
and category not in pfbudget.categories.groups["income-extra"] and category not in pfbudget.core.categories.groups["income-extra"]
and category not in pfbudget.categories.groups["investment"] and category not in pfbudget.core.categories.groups["investment"]
): ):
if category == "Null": if category == "Null":
if value != 0: if value != 0:

View File

View File

@ -53,7 +53,6 @@ def find_credit_institution(fn, banks, creditcards):
if bank.lower() not in [bank.lower() for bank in banks]: if bank.lower() not in [bank.lower() for bank in banks]:
raise BankNotAvailableError(f"{fn} -> {bank}: {banks}") raise BankNotAvailableError(f"{fn} -> {bank}: {banks}")
if cc and cc.lower() not in [cc.lower() for cc in creditcards]: if cc and cc.lower() not in [cc.lower() for cc in creditcards]:
# raise CreditCardNotAvailableError(f"{fn}: {banks}")
print(f"{fn} -> {cc} not in {creditcards}, using {bank} parser") print(f"{fn} -> {cc} not in {creditcards}, using {bank} parser")
cc = None cc = None