Adds command line option to add/remove categories

Implements the argument parser, the manager logic and the DB client
methods.
Encapsulates the DbClient connection under the _db attribute on the
manager.

Adds verbose option to enable ORM increased logging.
This commit is contained in:
Luís Murta 2022-12-08 00:09:28 +00:00
parent 882a77d24c
commit 9d33df78a8
Signed by: satprog
GPG Key ID: 169EF1BBD7049F94
6 changed files with 172 additions and 28 deletions

View File

@ -2,4 +2,4 @@ from pfbudget import Manager, run
if __name__ == "__main__":
command, args = run()
Manager(command).start(args)
Manager(command, args).start()

View File

@ -2,7 +2,7 @@ from pathlib import Path
import argparse
import re
from pfbudget.common.types import Command
from pfbudget.common.types import Command, Operation
from pfbudget.core.categories import categorize_data
from pfbudget.input.json import JsonParser
from pfbudget.input.nordigen import NordigenInput
@ -40,6 +40,9 @@ def argparser() -> argparse.ArgumentParser:
help.add_argument(
"-q", "--quiet", action="store_true", help="reduces the amount of verbose"
)
help.add_argument(
"-v", "--verbose", action="store_true", help="increases the amount of verbose"
)
period = argparse.ArgumentParser(add_help=False).add_mutually_exclusive_group()
period.add_argument(
@ -246,6 +249,56 @@ def argparser() -> argparse.ArgumentParser:
# func=lambda args: manager.parser(JsonParser(vars(args)))
# )
# Add category
p_categories = subparsers.add_parser(
"category",
parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
p_categories_commands = p_categories.add_subparsers(dest="command", required=True)
p_categories_add = p_categories_commands.add_parser(
"add",
parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
p_categories_add.add_argument("category", nargs="+", type=str)
p_categories_add.add_argument("--group", nargs="?", type=str)
p_categories_add.set_defaults(command=Command.Category, op=Operation.Add)
p_categories_remove = p_categories_commands.add_parser(
"remove",
parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
p_categories_remove.add_argument("category", nargs="+", type=str)
p_categories_remove.add_argument("--group", nargs="?", type=str)
p_categories_remove.set_defaults(command=Command.Category, op=Operation.Remove)
p_categories_addgroup = p_categories_commands.add_parser(
"addgroup",
parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
p_categories_addgroup.add_argument("group", nargs="+", type=str)
p_categories_addgroup.set_defaults(command=Command.Category, op=Operation.AddGroup)
p_categories_removegroup = p_categories_commands.add_parser(
"removegroup",
parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
p_categories_removegroup.add_argument("group", nargs="+", type=str)
p_categories_removegroup.set_defaults(command=Command.Category, op=Operation.RemoveGroup)
p_categories_updategroup = p_categories_commands.add_parser(
"updategroup",
parents=[help],
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
p_categories_updategroup.add_argument("category", nargs="+", type=str)
p_categories_updategroup.add_argument("--group", nargs=1, type=str)
p_categories_updategroup.set_defaults(command=Command.Category, op=Operation.UpdateGroup)
return parser

View File

@ -13,6 +13,15 @@ class Command(Enum):
Unregister = auto()
Token = auto()
Renew = auto()
Category = auto()
class Operation(Enum):
Add = auto()
Remove = auto()
AddGroup = auto()
RemoveGroup = auto()
UpdateGroup = auto()
class TransactionError(Exception):

View File

@ -2,7 +2,8 @@ from pfbudget.input.input import Input
from pfbudget.input.nordigen import NordigenClient
from pfbudget.input.parsers import parse_data
from pfbudget.db.client import DbClient
from pfbudget.common.types import Command
from pfbudget.db.model import Category, CategoryGroup
from pfbudget.common.types import Command, Operation
from pfbudget.core.categorizer import Categorizer
from pfbudget.utils import convert
@ -10,8 +11,9 @@ from pfbudget.cli.runnable import download, parse
class Manager:
def __init__(self, command: Command):
def __init__(self, command: Command, args: dict):
self.__command = command
self._args = args
match (command):
case Command.Init:
pass
@ -29,22 +31,24 @@ class Manager:
pass
case Command.Renew:
pass
case Command.Category:
pass
def start(self, args):
assert "database" in args, "ArgParser didn't include db"
self._db = args["database"]
def start(self):
match (self.__command):
case Command.Init:
pass
case Command.Parse:
# TODO this is a monstrosity, remove when possible
self._db = DbClient(args["database"])
parse(self, args)
parse(self, self.args)
case Command.Download:
# TODO this is a monstrosity, remove when possible
self._db = DbClient(args["database"])
download(self, args)
download(self, self.args)
case Command.Categorize:
self._db = DbClient(args["database"])
self.categorize(args)
self.categorize(self.args)
case Command.Register:
# self._db = DbClient(args["database"])
# self.register(args)
@ -55,29 +59,72 @@ class Manager:
pass
case Command.Token:
NordigenClient(self).token()
case Command.Renew:
NordigenClient(self).requisition(args["name"], args["country"])
NordigenClient(self).requisition(
self.args["name"], self.args["country"]
)
case Command.Category:
assert "op" in self.args, "category operation not defined"
with self.db.session() as session:
match self.args["op"]:
case Operation.Add:
for category in self.args["category"]:
session.addcategory(
Category(name=category, group=self.args["group"])
)
case Operation.Remove:
session.removecategory(
[
Category(name=category)
for category in self.args["category"]
]
)
case Operation.UpdateGroup:
session.updategroup(
[
Category(name=category)
for category in self.args["category"]
],
self.args["group"][0],
)
case Operation.AddGroup:
for group in self.args["group"]:
session.addcategorygroup(CategoryGroup(name=group))
case Operation.RemoveGroup:
session.removecategorygroup(
[
CategoryGroup(name=group)
for group in self.args["group"]
]
)
# def init(self):
# client = DatabaseClient(self.__db)
# client.init()
# def register(self, args: dict):
# bank = Bank(args["bank"][0], "", args["requisition"][0], args["invert"])
# def register(self):
# bank = Bank(self.args["bank"][0], "", self.args["requisition"][0], self.args["invert"])
# client = DatabaseClient(self.__db)
# client.register_bank(convert(bank))
# def unregister(self, args: dict):
# def unregister(self):
# client = DatabaseClient(self.__db)
# client.unregister_bank(args["bank"][0])
# client.unregister_bank(self.args["bank"][0])
def parser(self, parser: Input):
transactions = parser.parse()
print(transactions)
# self.add_transactions(transactions)
# def parse(self, filename: str, args: dict):
# transactions = parse_data(filename, args)
# def parse(self, filename: str):
# transactions = parse_data(filename, self.args)
# self.add_transactions(transactions)
# def transactions() -> list[Transaction]:
@ -86,13 +133,11 @@ class Manager:
def add_transactions(self, transactions):
with self.db.session() as session:
session.add(transactions)
session.commit()
def categorize(self, args: dict):
def categorize(self):
with self.db.session() as session:
uncategorized = session.uncategorized()
Categorizer().categorize(uncategorized)
session.commit()
# def get_bank_by(self, key: str, value: str) -> Bank:
# client = DatabaseClient(self.__db)
@ -103,5 +148,13 @@ class Manager:
return self.db.get_nordigen_banks()
@property
def db(self):
return self._db
def db(self) -> DbClient:
return DbClient(self._db, self.args["verbose"])
@db.setter
def db(self, url: str):
self._db = url
@property
def args(self) -> dict:
return self._args

View File

@ -1,7 +1,7 @@
from sqlalchemy import create_engine, select
from sqlalchemy import create_engine, delete, select, update
from sqlalchemy.orm import Session, joinedload, selectinload
from pfbudget.db.model import Bank, Category, Transaction
from pfbudget.db.model import Bank, Category, CategoryGroup, Transaction
# import logging
@ -16,8 +16,8 @@ class DbClient:
__sessions: list[Session]
def __init__(self, url: str) -> None:
self._engine = create_engine(url)
def __init__(self, url: str, echo=False) -> None:
self._engine = create_engine(url, echo=echo)
def get_transactions(self):
"""¿Non-optimized? get_transactions, will load the entire Transaction"""
@ -65,6 +65,7 @@ class DbClient:
return self
def __exit__(self, exc_type, exc_value, exc_tb):
self.commit()
self.__session.close()
def commit(self):
@ -76,9 +77,32 @@ class DbClient:
def addcategory(self, category: Category):
self.__session.add(category)
def removecategory(self, categories: list[Category]):
stmt = delete(Category).where(
Category.name.in_([cat.name for cat in categories])
)
self.__session.execute(stmt)
def updategroup(self, categories: list[Category], group: CategoryGroup):
stmt = (
update(Category)
.where(Category.name.in_([cat.name for cat in categories]))
.values(group=group)
)
self.__session.execute(stmt)
def addcategorygroup(self, group: CategoryGroup):
self.__session.add(group)
def removecategorygroup(self, groups: list[CategoryGroup]):
stmt = delete(CategoryGroup).where(
CategoryGroup.name.in_([grp.name for grp in groups])
)
self.__session.execute(stmt)
def uncategorized(self) -> list[Transaction]:
stmt = select(Transaction).where(~Transaction.category.has())
return self.__session.scalars(stmt).all()
def session(self):
def session(self) -> ClientSession:
return self.ClientSession(self.engine)

View File

@ -108,6 +108,11 @@ class Category(Base):
cascade="all, delete-orphan", passive_deletes=True
)
def __repr__(self) -> str:
return (
f"Category(name={self.name}, group={self.group}, #rules={len(self.rules)})"
)
class TransactionCategory(Base):
__tablename__ = "categorized"