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"
__version__ = "0.1"
from .categories import categorize_data
from .parsers import parse_data
from .runnable import run
from pfbudget.core.categories import categorize_data
from pfbudget.core.input.parsers import parse_data
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 re
from .categories import categorize_data
from .database import DBManager
from .parsers import parse_data
import pfbudget.graph
import pfbudget.report
from pfbudget.core.categories import categorize_data
from pfbudget.core.input.parsers import parse_data
from pfbudget.db.client import DBManager
import pfbudget.reporting.graph
import pfbudget.reporting.report
import pfbudget.utils
from pfbudget.core.input.nordigen import Client
DEFAULT_DB = "data.db"
@ -150,6 +153,56 @@ def argparser() -> argparse.ArgumentParser:
)
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
@ -178,11 +231,11 @@ def graph(args):
"""
start, end = pfbudget.utils.parse_args_period(args)
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":
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":
pfbudget.graph.networth(DBManager(args.database), vars(args), start, end)
pfbudget.reporting.graph.networth(DBManager(args.database), vars(args), start, end)
def report(args):
@ -193,9 +246,9 @@ def report(args):
"""
start, end = pfbudget.utils.parse_args_period(args)
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":
pfbudget.report.detailed(DBManager(args.database), start, end)
pfbudget.reporting.report.detailed(DBManager(args.database), start, end)
def run():

View File

View File

@ -8,8 +8,8 @@ import yaml
if TYPE_CHECKING:
from pfbudget.database import DBManager
from pfbudget.transactions import Transaction
from pfbudget.db.manager import DBManager
from pfbudget.core.transactions import Transaction
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 decimal import Decimal
from importlib import import_module
from typing import TYPE_CHECKING
import datetime as dt
import yaml
from .transactions import Transaction
from . import utils
if TYPE_CHECKING:
from .database import DBManager
from pfbudget.core.transactions import Transaction
from pfbudget.utils import utils
Index = namedtuple(
"Index", ["date", "text", "value", "negate"], defaults=[-1, -1, -1, False]

View File

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

View File

@ -3,10 +3,10 @@ from dateutil.rrule import rrule, YEARLY
from typing import TYPE_CHECKING
import datetime as dt
import pfbudget.categories
import pfbudget.core.categories
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):
@ -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
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 [
@ -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")
def detailed(db: DBManager, start: dt.date = dt.date.min, end: dt.date = dt.date.max):
transactions = db.get_daterange(start, end)
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
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 [
@ -93,23 +94,23 @@ def detailed(db: DBManager, start: dt.date = dt.date.min, end: dt.date = dt.date
income = sum(
sum
for category, sum in categories.items()
if category in pfbudget.categories.groups["income-fixed"]
or category in pfbudget.categories.groups["income-extra"]
if category in pfbudget.core.categories.groups["income-fixed"]
or category in pfbudget.core.categories.groups["income-extra"]
)
print(f"Income: {income:.2f}\n")
investments = -sum(
sum
for category, sum in categories.items()
if category in pfbudget.categories.groups["investment"]
if category in pfbudget.core.categories.groups["investment"]
)
expenses = 0
for category, value in categories.items():
if (
category not in pfbudget.categories.groups["income-fixed"]
and category not in pfbudget.categories.groups["income-extra"]
and category not in pfbudget.categories.groups["investment"]
category not in pfbudget.core.categories.groups["income-fixed"]
and category not in pfbudget.core.categories.groups["income-extra"]
and category not in pfbudget.core.categories.groups["investment"]
):
if category == "Null":
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]:
raise BankNotAvailableError(f"{fn} -> {bank}: {banks}")
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")
cc = None