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:
parent
2af9b6ebc6
commit
4b5773aa13
@ -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
4
pfbudget/__main__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from pfbudget.cli.runnable import run
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
0
pfbudget/cli/__init__.py
Normal file
0
pfbudget/cli/__init__.py
Normal 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():
|
||||
0
pfbudget/core/__init__.py
Normal file
0
pfbudget/core/__init__.py
Normal 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(
|
||||
0
pfbudget/core/input/__init__.py
Normal file
0
pfbudget/core/input/__init__.py
Normal file
74
pfbudget/core/input/nordigen.py
Normal file
74
pfbudget/core/input/nordigen.py
Normal 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)
|
||||
@ -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]
|
||||
@ -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():
|
||||
0
pfbudget/reporting/__init__.py
Normal file
0
pfbudget/reporting/__init__.py
Normal 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")
|
||||
@ -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:
|
||||
0
pfbudget/utils/__init__.py
Normal file
0
pfbudget/utils/__init__.py
Normal 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user