diff --git a/pfbudget/__init__.py b/pfbudget/__init__.py index 6a66eb8..90815a9 100644 --- a/pfbudget/__init__.py +++ b/pfbudget/__init__.py @@ -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 diff --git a/pfbudget/__main__.py b/pfbudget/__main__.py new file mode 100644 index 0000000..eba7182 --- /dev/null +++ b/pfbudget/__main__.py @@ -0,0 +1,4 @@ +from pfbudget.cli.runnable import run + +if __name__ == "__main__": + run() diff --git a/pfbudget/cli/__init__.py b/pfbudget/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pfbudget/runnable.py b/pfbudget/cli/runnable.py similarity index 68% rename from pfbudget/runnable.py rename to pfbudget/cli/runnable.py index 365f9f3..63f860d 100644 --- a/pfbudget/runnable.py +++ b/pfbudget/cli/runnable.py @@ -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(): diff --git a/pfbudget/core/__init__.py b/pfbudget/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pfbudget/categories.py b/pfbudget/core/categories.py similarity index 98% rename from pfbudget/categories.py rename to pfbudget/core/categories.py index ce7f319..4af5abc 100644 --- a/pfbudget/categories.py +++ b/pfbudget/core/categories.py @@ -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( diff --git a/pfbudget/core/input/__init__.py b/pfbudget/core/input/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pfbudget/core/input/nordigen.py b/pfbudget/core/input/nordigen.py new file mode 100644 index 0000000..64107af --- /dev/null +++ b/pfbudget/core/input/nordigen.py @@ -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) diff --git a/pfbudget/parsers.py b/pfbudget/core/input/parsers.py similarity index 96% rename from pfbudget/parsers.py rename to pfbudget/core/input/parsers.py index 6fab891..f45d1c2 100644 --- a/pfbudget/parsers.py +++ b/pfbudget/core/input/parsers.py @@ -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] diff --git a/pfbudget/transactions.py b/pfbudget/core/transactions.py similarity index 100% rename from pfbudget/transactions.py rename to pfbudget/core/transactions.py diff --git a/pfbudget/database.py b/pfbudget/db/manager.py similarity index 99% rename from pfbudget/database.py rename to pfbudget/db/manager.py index 2d5705b..ad6ad8a 100644 --- a/pfbudget/database.py +++ b/pfbudget/db/manager.py @@ -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(): diff --git a/pfbudget/reporting/__init__.py b/pfbudget/reporting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pfbudget/graph.py b/pfbudget/reporting/graph.py similarity index 79% rename from pfbudget/graph.py rename to pfbudget/reporting/graph.py index a7ea9bc..9e9ca1f 100644 --- a/pfbudget/graph.py +++ b/pfbudget/reporting/graph.py @@ -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") diff --git a/pfbudget/report.py b/pfbudget/reporting/report.py similarity index 85% rename from pfbudget/report.py rename to pfbudget/reporting/report.py index d58e605..b181d6e 100644 --- a/pfbudget/report.py +++ b/pfbudget/reporting/report.py @@ -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: diff --git a/pfbudget/utils/__init__.py b/pfbudget/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pfbudget/utils.py b/pfbudget/utils/utils.py similarity index 97% rename from pfbudget/utils.py rename to pfbudget/utils/utils.py index 03fcdae..7e692f6 100644 --- a/pfbudget/utils.py +++ b/pfbudget/utils/utils.py @@ -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